DS博客作业04--图
0.PTA得分截图
1.本周学习总结
1.1 总结图内容
-
图存储结构:图的存储结构分为两种,邻接矩阵和邻接表
- 邻接矩阵:邻接矩阵是用一个二维数组存储边信息的矩阵,该种存储方式比较适合稠密图
邻接矩阵的结构体:typedef struct //图的定义 { int edges[MAXV][MAXV]; //邻接矩阵 int n, e; //顶点数,弧数 } MGraph; //图的邻接矩阵表示类型
- 邻接表:邻接表将所有表头节点构成一个数组,在给每个表头节点建一个单链表,将对应表头节点的所有邻接点链起来。
邻接表的结构体:typedef struct { AdjList adjlist; //邻接表 int n, e; //图中顶点数n和边数e } AdjGraph;
- 邻接矩阵:邻接矩阵是用一个二维数组存储边信息的矩阵,该种存储方式比较适合稠密图
-
图遍历:图的遍历也分为两种,分别是:深度优先遍历(DFS)和广度优先遍历(BFS)
- 深度优先遍历:深度优先遍历是指从一个顶点v出发,选择一个与顶点v相邻且没被访问过的顶点w为初始顶点,再从w出发进行深度优先搜索,直到图中与当前顶点v邻接的所有顶点都被访问过为止。
其代码实现为:void DFS(MGraph g, int v)//深度遍历 { int j; static int flag = 0; if (visited[v] == 0) { visited[v] = 1; if (flag == 0) { cout << v; flag = 1; } else { cout << " " << v; } } for (j = 1; j <= g.n; j++) { if (g.edges[v][j] && visited[j] == 0) //边存在且j节点未被访问过 { DFS(g, j); } } }
- 广度优先遍历:广度优先遍历是指从一个顶点v出发,访问v的所有未被访问过的邻接点。然后按照次序访问每一个顶点的所有未被访问过的邻接点,直到图中所有的顶点都被访问过。
其代码实现为:void BFS(MGraph g, int v)//广度遍历 { int j; queue<int> q; if (visited[v] == 0) { visited[v] = 1; cout << v; q.push(v); } while (!q.empty()) { v = q.front(); q.pop(); for (j = 1; j <= g.n; j++) { if (g.edges[v][j] && visited[j] == 0) //边存在且j节点未被访问过 { visited[j] = 1; cout << " " << j; q.push(j); } } } }
- 深度优先遍历:深度优先遍历是指从一个顶点v出发,选择一个与顶点v相邻且没被访问过的顶点w为初始顶点,再从w出发进行深度优先搜索,直到图中与当前顶点v邻接的所有顶点都被访问过为止。
-
图的应用:图的应用有最小生成树、最短路径等。
-
最小生成树相关算法:最小生成树是指在一个图中找到一棵权值之和最小的生成树。其可应用于现实生活中公路的建设,我们可以利用最小生成树找到最节省经费的公路建发。
-
Prim算法:
其伪代码为:初始化lowcost,closest数组 for(v=1;v<=n;v++) 遍历lowcost数组 //选最小边 若lowcost[i]!=0,找最小边 找最小边对应邻接点k 最小边lowcost[k]=0; 输出边(closest[k],k); 遍历lowcost数组 //修正lowcost 若lowcost[i]!=0 && edges[i][k]<lowcost[k] 修正lowcost[k]=edges[i][k] 修正closest[j]=k; end
其实现代码为:
void Prim(MGraph G) { int min, i, j, k; int adjvex[MAXVEX]; int lowcost[MAXVEX]; lowcost[0] = 0; adjvex[0] = 0; for (i = 1; i < G.numVertexes; i++) { lowcost[i] = G.arc[0][i]; adjvex[i] = 0; } for (i = 1; i < G.numVertexes; i++) { min = INIFINTY; j = 1; k = 0; while (j < G.numVertexes) { if (lowcost[j] != 0 && lowcost[j] < min) { min = lowcost[j]; k = j; } j++; } printf("(%d, %d)\n", adjvex[k], k); lowcost[k] = 0; for (j = 1; j < G.numVertexes; j++) { if (lowcost[j] != 0 && G.arc[k][j] < lowcost[j]) { lowcost[j] = G.arc[k][j]; adjvex[j] = k; } } } }
-
Kruskal算法:
其伪代码为:构造数组E存储图中所有的边 E中所有边按权重排序。(堆排序,快排序) 构造集合 ST=( V,{ } ); while (k<n-1) { 检查边集 E 中第 i 条权值最小的边(u,v); 若(u,v)加入ST后不使ST中产生回路,则输出边(u,v); }
其实现代码为:
void Kruskal(MGraph G) { int i, j, n, m; int k = 0; Edge edges[MAXEDGE]; int parent[MAXVEX]; for (i = 0; i < G.numVertexes - 1; i++) { for (j = i + 1; j < G.numVertexes; j++) { if (G.arc[i][j] < INIFINTY) { edges[k].begin = i; edges[k].end = j; edges[k].weight = G.arc[i][j]; k++; } } } sort(edges, &G); for (i = 0; i < G.numVertexes; i++) { parent[i] = 0; } for (i = 0; i < G.numEdges; i++) { n = Find(parent, edges[i].begin); m = Find(parent, edges[i].end); if (n != m) { parent[n] = m; printf("(%d, %d) %d\n", edges[i].begin, edges[i].end, edges[i].weight); } } }
-
最小生成树的应用有公路村村通:根据有可能建设成标准公路的若干条道路的成本,想要得到每个村落都有公路连通所需要的最低成本我们就可以使用最小生成树的思路来解决该问题。
其伪代码思路为:根据题目所给数据建立一个邻接矩阵存储的图结构 然后调用Prim void Prim(MGraph g, int v) { 定义一个count用于记录被选中的节点数,初始值置为1 定义变量k,初始值置为0 初始化lowcost数组 Lowcost [1]=0; //从1开始遍历,1已经被选中 For i=1 to i<g.n do 遍历lowcost数组 找最小边,并将对应节点赋值给k If(k==0) Break; Sum+=lowcost[k]; Count++; Lowcost[k]=0; 遍历lowcost数组 若lowcost[j]!=0 && g.edges[k][j]<lowcost[j] lowcost[j] = g.edges[k][j]; K=0; End for If count不等于g.n then 输出-1 Else 输出sum End if }
其实现代码为:
void Prim(MGraph g, int v) { int lowcost[MAXV]; int min, i, j, k; int count = 1; //用于记录加入U的顶点个数 int sum = 0; k = 0; for (i = 1; i <= g.n; i++) { lowcost[i] = g.edges[v][i]; } lowcost[1] = 0; for (i = 1; i < g.n; i++) { min = INF; for (j = 1; j <= g.n; j++) { if (lowcost[j] != 0 && lowcost[j] < min) { min = lowcost[j]; k = j; } } if (k == 0) break; sum += lowcost[k]; lowcost[k] = 0; //k节点加入U count++; for (j = 1; j <= g.n; j++) { if (lowcost[j] != 0 && g.edges[k][j] < lowcost[j]) { lowcost[j] = g.edges[k][j]; } } k = 0; } if (count != g.n) { cout << "-1"; } else { cout << sum; } }
-
-
最短路径相关算法:在一个图中,找到一条从一个顶点到另一个顶点的最短路径。
- Dijkstra算法:用于求一个顶点到其余各个顶点之间的最短路径
其伪代码为:
其实现代码为:初始化dist数组、path数组、s数组 遍历图中所有节点 { for (i = 0; i < g.n; i++) //找最短dist { 若s[i] != 0,则dist数组找最短路径,顶点为u } s[u] = 1 //加入集合S,顶点已选 for (i = 0; i < g.n; i++) //修正dist { 若s[i] != 0 && dist[i] > dist[u] + g.edges[u][i] 则修正dist[i] = dist[i] > dist[u] + g.edges[u][i] path[i] = u; } }
void Dijkstra(MatGraph g,int v) { int dist[MAXV],path[MAXV]; int s[MAXV]; int mindis, i, j, u; for (i = 0; i < g.n; i++) { dist[i] = g.edges[v][i]; //距离初始化 s[i] = 0; //s[]置空 if (g.edges[v][i] < INF) //路径初始化 path[i] = v; //顶点v到i有边时 else path[i] = -1; //顶点v到i没边时 } s[v] = 1; //源点v放入S中 for (i = 0; i < g.n; i++) //循环n-1次 { mindis = INF; for (j = 0; j < g.n; j++) if (s[j] == 0 && dist[j] < mindis) { u = j; mindis = dist[j]; } s[u] = 1; //顶点u加入S中 for (j = 0; j < g.n; j++) //修改不在s中的顶点的距离 if (s[j] == 0) if (g.edges[u][j] < INF && dist[u] + g.edges[u][j] < dist[j]) { dist[j] = dist[u] + g.edges[u][j]; path[j] = u; } } Dispath(dist, path, s, g.n, v); //输出最短路径 }
- Floyd算法:用于求任意两个顶点之间的最短路径
其实现代码为:void Floyd(MatGraph g) //求每对顶点之间的最短路径 { int A[MAXVEX][MAXVEX]; //建立A数组 int path[MAXVEX][MAXVEX]; //建立path数组 int i, j, k; for (i = 0; i < g.n; i++) for (j = 0; j < g.n; j++) { A[i][j] = g.edges[i][j]; if (i != j && g.edges[i][j] < INF) path[i][j] = i; //i和j顶点之间有一条边时 else //i和j顶点之间没有一条边时 path[i][j] = -1; } for (k = 0; k < g.n; k++) //求Ak[i][j] { for (i = 0; i < g.n; i++) for (j = 0; j < g.n; j++) if (A[i][j] > A[i][k] + A[k][j]) //找到更短路径 { A[i][j] = A[i][k] + A[k][j]; //修改路径长度 path[i][j] = k; //修改经过顶点k } } }
- 最短路径的相关应用有旅游规划:当我们知道城市间的高速公路长度、以及该公路要收取的过路费时,如何选择一条出发地和目的地之间的最短路径,且当最短路径有多条时,如何选择收费最少的路径,这就可以用到最短路径相关算法来解决该问题。
其伪代码思路为:
其实现代码为:根据题目所给数据建立一个邻接矩阵存储的图结构 调用Dijkstra函数 输出dist[目的地编号]和costMin[目的地编号] void Dijkstra(MGraph g, int v) { 初始化dist数组、s数组、costMin数组 遍历图中所有节点 { for(i = 0; i < g.n; i++) { 若s[i]! = 0,则数组找最短路径,顶点为u } s[u] = 1 for(i = 0; i < g.n; i++) { if(g.edges[u][j] < INF && dist[u] + g.edges[u][j] < dist[j]) 则修正dist[j] = dist[u] + g.edges[u][j]; costMin[j] = costMin[u] + cost[u][j]; else if(路径一样长但是花费更少) 则修正costMin[j] = costMin[u] + cost[u][j]; end if } } }
void Dijkstra(MGraph g, int v) { int s[MAXV]; int mindis, i, j, u; for (i = 0; i < g.n; i++) { dist[i] = g.edges[v][i]; s[i] = 0; costMin[i] = cost[v][i]; } s[v] = 1; //顶点v加入s costMin[v] = 0; for (i = 0; i < g.n; i++) { mindis = INF; for (j = 0; j < g.n; j++) { if (s[j] == 0 && dist[j] < mindis) //找最短路径长度顶点u { u = j; mindis = dist[j]; } } s[u] = 1; //顶点u加入s for (j = 0; j < g.n; j++) { if (g.edges[u][j] < INF && dist[u] + g.edges[u][j] < dist[j]) { dist[j] = dist[u] + g.edges[u][j]; costMin[j] = costMin[u] + cost[u][j]; } else if (dist[u] + g.edges[u][j] == dist[j] && costMin[j] > costMin[u] + cost[u][j]) //路径一样长但是花费更少时要调整 { costMin[j] = costMin[u] + cost[u][j]; } } } }
- Dijkstra算法:用于求一个顶点到其余各个顶点之间的最短路径
-
拓扑排序:拓扑序列是指,每个顶点出现且只出现一次,若有一条从A到B的路径,则在拓扑序列中,A必须在B的前面。(注意:只有有向无环图才有拓扑排序,故拓扑排序可用于检测一个有向图是否有环)
其算法思路为:遍历邻接表 计算每个顶点的入度,存入头结点count成员 遍历图顶点 若发现入度为0顶点,入栈st while(栈不空) { 出栈节点v,访问。 遍历v的所有邻接点 { 所有邻接点的入度-1 若有邻接点入度为0,则入栈st } }
-
关键路径:关键路径是一个图中的最长路径,关键路径的长度会决定整个项目的工期。
1.2 谈谈你对图的认识及学习体会
图的结构比我们之前学的都要更加复杂,因为之前的线性结构或者树,都是一对一或一对多的关系,而图是多对多的关系,那么在存储方式和遍历上都会跟之前学的有比较大的区别。
2.阅读代码
2.1 细分图中的可到达结点
题目:
解题代码:
class Solution {
public:
int reachableNodes(vector<vector<int>>& edges, int M, int N) {
int res = 0;
vector<vector<int>> graph(N, vector<int>(N, -1));
vector<bool> visited(N);
priority_queue<pair<int, int>> pq;
pq.push({M, 0});
for (auto &edge : edges) {
graph[edge[0]][edge[1]] = edge[2];
graph[edge[1]][edge[0]] = edge[2];
}
while (!pq.empty()) {
auto t= pq.top(); pq.pop();
int move = t.first, cur = t.second;
if (visited[cur]) continue;
visited[cur] = true;
++res;
for (int i = 0; i < N; ++i) {
if (graph[cur][i] == -1) continue;
if (move > graph[cur][i] && !visited[i]) {
pq.push({move - graph[cur][i] - 1, i});
}
graph[i][cur] -= min(move, graph[cur][i]);
res += min(move, graph[cur][i]);
}
}
return res;
}
};
2.1.1 该题的设计思路
该题采用Dijkstra算法,该算法的一般形式是用一个最小堆来保存到源点的最小距离,这里我们直接统计到源点的最小距离不是很方便,可以使用一个小 trick,即用一个最大堆来统计当前结点所剩的最大步数,因为剩的步数越多,说明距离源点距离越小。再建立一个二维数组graph,其中 graph[i][j] 表示从大结点i往大结点j方向会经过的小结点个数。另外由于会重复计算小节点的问题,此题虽然是无向图,但我们将其当做有向图来做。
2.1.2 该题的伪代码
定义一个res用于计数,初始化为0
建立一个栈,并初始化为{M,0},表示剩余M步,当前节点为0(栈结构为{move,cur},move表示当前剩余步数,cur表示当前节点)
建立一个二维数组graph,graph[i][j] 表示从大结点i往大结点j方向会经过的小结点个数
while(栈不为空)
{
栈顶元素出栈,并赋值给t
if(t.cur已被遍历) continue;
res++;
遍历所有节点
{
if(graph[t.cur][i]==-1) continue;//两个大节点之间不存在小节点
if(剩余步数>graph[t.cur][i] && 节点i未被遍历过)
{
{move - graph[cur][i] - 1, i}入栈
}
graph[i][cur] -= min(move, graph[cur][i]); //调整graph数组
res += min(move, graph[cur][i]);
}
}
2.1.3 运行结果
2.1.4 分析该题目解题优势及难点
解题优势:建立辅助数组graph记录两个大节点间的小节点数,并用其和剩余步数做比较,不需要新建一个图,且思路更加简单,易理解。
难点:这题类似于六度空间,但该题相较于六度空间,它在原始图上又新加了节点进去构成了一个新图,如何构成新图是个难点。
2.2 给 N x 3 网格图涂色的方案数
题目:
解题代码:
#include<iostream>
using namespace std;
int main()
{
int n;
int Max_num = 1e9 + 7;
long int k = 6, m = 6, result = 12, temp;//第一列,第二列,结果总数,临时k值存储
cin >> n;
for (int i = 2; i <= n; ++i)
{
result = (((k * 5) % Max_num) + ((m * 4) % Max_num)) % Max_num;//计算总数
temp = k;
k = (((k * 3) % Max_num) + ((m * 2) % Max_num)) % Max_num;//更新k
m = (((temp * 2) % Max_num) + ((m * 2) % Max_num)) % Max_num;//更新m
}
cout << result;
return 0;
}
2.2.1 该题的设计思路
当第一层只使用2种颜色(即ABA型)时,它的下一行可以有5种组合(BCB、BAB、CAC、CAB、BAC),其中ABA型3种,ABC型2种;当第一层使用3种颜色(即ABC型)时,它的下一行可以有4种组合(BCB、BAB、BCA、CAB),其中ABA型2种,ABC型2种。则可设一层的ABA型有k种,ABC型有m种,则下一行的结果为k5+m4
2.2.2 该题的伪代码
定义k,m,result,分别用于存储第i行的ABA型种数,第i行的ABC型种数,和第i行的结果
定义temp用于存储未更新时k的值
将k,m初始化为6,result初始化为12
for i=2 to i=n do
result = k*5+m*4;
temp=k;
k=k*3+m*2
m=temp*2+m*2;
end for
输出result
2.2.3 运行结果
2.2.4 分析该题目解题优势及难点
优点:通过动态规划分析该题,得出规律,使该题变得十分简单。
难点:该题难点在于,当n比较大时,需要考虑到的就会比较多,除了保证每行的相邻元素不同外,还要考虑竖直方向上也不能相邻,且要统计出所有可能的结果。
2.3 克隆图
题目:
解题代码:
class Solution {
public:
Node* cloneGraph(Node* node) {
if(node==NULL) return NULL;
map<Node*,Node*> mp;
queue<Node*> old_q;
old_q.push(node);
vector<Node*> neighbors;
Node* res=new Node(node->val,neighbors);
mp[node]=res;
while(!old_q.empty()){
Node* old_temp=old_q.front();
old_q.pop();
for(int i=0;i<old_temp->neighbors.size();i++){
if(mp.find(old_temp->neighbors[i])==mp.end()){
vector<Node*> neighbors;
Node* new_node=new Node(old_temp->neighbors[i]->val,neighbors);
mp[old_temp->neighbors[i]]=new_node;
old_q.push(old_temp->neighbors[i]);
}
mp[old_temp]->neighbors.push_back(mp[old_temp->neighbors[i]]);
}
}
return res;
}
};
2.3.1 该题的设计思路
深拷贝即复制原图中的结点和结点之间的边。用一个map记录原结点和新结点的键值对,以便于克隆图复制原图结点之间的边。广度优先搜索图,对于当前结点,复制其相邻结点(如果还没复制),用map记录和原结点的对应关系,然后再复制和当前结点的边。
2.3.2 该题的伪代码
建立一个map,用于记录原结点和新结点的键值对
建立一个队列old_q,现将原节点入队
新建节点res,mp[node]=res;
while(队列不为空)
{
队头元素出队,并赋值给old_temp
for i=0 to i=old_temp的邻居个数 do
if(mp.find(old_temp->neighbors[i])==mp.end())
{
新建节点,值为old_temp->neighbors[i]->val;
mp[old_temp->neighbors[i]]=new_node;
old_temp->neighbors[i]入队;
}
mp[old_temp]->neighbors.push_back(mp[old_temp->neighbors[i]]);
end for
}
2.3.3 运行结果
2.3.4 分析该题目解题优势及难点
解题优势:用map记录和原结点的对应关系,便于克隆图复制原图结点之间的边。
难点:本题虽然看着简单,只要将输入的数据再次输出就可以,但题目的要求是你不能返回一个相同的图,而是要新建一个图,使它和原图一样。