最小生成树
参考:https://www.bilibili.com/video/BV1Ut411976J?from=search&seid=6406725088142163166
参考:https://baike.baidu.com/item/%E8%BF%9E%E9%80%9A%E5%9B%BE/6460995?fr=aladdin
一,MST 性质 (Minimum Spaning Tree)
1,定义:
设 N = {V,E} 是一个连通网,U 是顶点集 V 的一个非空子集。若 (u, v) 是一条具有最小权值的边,其中 u∈U,v∈V-U,则必存在一棵包含 (u, v) 的最小生成树。
2,证明:
因为必然存在这么两颗未完成的最小生成树:一个顶点集为U;一个顶点集为 V-U。这两颗最小生成树要想得到完整的最小生成树,则最后一条边必然为 —— 其中一个顶点属于 U ,另一个顶点属于 V-U 的所有边中权值最小的边。
3,注意点
① MST 是构造最小生成树的基本性质
② 最小生成树并不唯一
③ 最小生成树只适用于无向图。若是有向图的话,选取不同的源点就会有不同的结果。
4,补充概念 —— 连通图
无向图中,若从 点i 到 点j 之间有路径,则称 点i 和 点j 之间是连通的;有向图中,若从 点i 到 点j 之间有路径,则称 点i 可达 点j 。
若无向图中任意两个顶点都是连通的,则称该图是连通图。若有向图中任意两个顶点都是互相可达的,则称该图是强连通图。
二,Prim 算法
1,算法思想
先利用 MST 性质,将图中的点分为两个集合:U (已落在最小生成树上的顶点集);V-U (未落在最小生成树上的顶点集)。
然后在连通 U 和 V-U 的所有边中选取权值最小的边,加入当前的最小生成树。
最后,不断循环选取,直至选取的边可以构成一颗最小生成树。
2,步骤
设 N = {V,E} 是一个连通图,M = {U,TE} 是 N 上的一棵最小生成树,源点为 u0,n = | V |。
再设属于 V-U 的 点i 与 U 中的所有点有直达边的所有边中的边长最短的边叫 —— 点i 到 U 的最短直达边。
① 初始化:令 U = { u0 },TE = { }。
② 更新距离:更新并记录 V-U 中的点到 U 的最短直达距离及对应的直达边。
③ 找最短边:在 V-U 中的点到 U 的所有最短直达边中,找出边长最短的边 (ui,vi)。
④ 更新 M:将 (ui,vi) 加入 TE 中,并将 vi 从 V-U 中拿出,加入 U。
⑤ 迭代循环:重复 n-1 次 ②,③,④ 的循环。
⑥ 判断:通过判断 U 是否等于 V 来判断是否能生成最小生成树。
说明1:为什么 ⑤迭代循环 要循环 n-1 次?
因为每次循环都要找到一条最小生成树的边,而 n 个顶点的最小生成树需要找到 n-1 条边。
说明2:为什么需要 ②更新距离?
因为每次 vi 加到 U 中,就可能会改变 V-U 中的点到 U 的最短直达边,所以要遍历 V-U 中的点到 vi 的距离,比较是否会比之前的最短直达边更短。
说明3:为什么要先 ②,再 ③④?
条件:
每次进行 ④更新M 后,都必须进行 ②更新距离,具体见说明2。其实,①初始化 可以看成是一次 ④更新M,所以在 ①初始化 之后,需要进行一次 ②更新距离。
原因:
有些人的做法是在循环外进行 ②更新距离,然后按 ③④② 的顺序进行 n-1 次循环。但其实最后一次循环的 ④更新M 之后进行的 ②更新距离 是没有意义的,因为此时最小生成树已经生成了。
于是,我们利用这点,将②提到③④前面,从而将最后一次无用功的 ②更新距离 提前到第一次循环开头。利用第一次循环的 ②更新距离 对 ①初始化 进行更新距离,从而不用在循环外面特意更新距离。
3,数据结构
存图常用的链式前向星或邻接表或邻接矩阵,效果一样,效率不同。
dis[]:
区分点是属于 V-U 还是 U:
若 dis[i] > 0,则 i 属于 V-U;
若 dis[i] == 0,则 i 属于 U。
记录距离:
若 i 属于 U,则 dis[i] 并无意义
若 i 属于 V-U,代表 点i 到 U 的最短直达边距离
p[]:
表示 最小生成树
若 p[i] == -1,则代表 i 是 u0 ;
若 p[i] != -1,则代表 i 的父节点是 p[i] ;
4,代码及注意事项
Ⅰ,源点的选取是任意的,可以选取图中任意点作为源点。
Ⅱ,初始化时 s 是源点。经过后面的更新和选择后,s 就代表循环中每次加入 U 中的新的点。
Ⅲ,在 ②更新距离 中,p[] 是将 V-U 中的点到 U 的最短直达边全都记录下来。虽然,每次记录下来的时候,只有1条边是正确的。但在进行 ⑤迭代循环 后,p[] 记录的就都是最小生成树的边了。
Ⅳ,如果在某次 ③找最短边 中,如果找到的边的权值是 inf,说明此时 U 和 V-U 之间不存在连通的边,即此时的 U 和 V-U 是两个不同的连通分量,即该图不是连通图,即该图不存在最小生成树。
Ⅴ,如果是运行了多次的 Prim() 算法的话,p[] 的初始化只需要 p[u0] = -1 就可以了。因为最小生成树是必然连通的,所以即使 p[] 的值在前一次 Prim() 算法中被赋值了,也会在 ②更新距离 中重新赋值。
Ⅵ,p[] 表示的是小生成树,其根节点是源点。
如果代码中是 p[to] = s; —— V-U 中的点指向 U 中的点,则树的方向是叶子节点指向根节点。
如果代码中是 p[s] = to; —— U 中的点指向 V-U 中的点,则树的方向是根节点指向叶子节点。
Ⅶ,用优先队列时,无法根据 BFS 的结束条件判断图是否为连通图。所以,需要根据 dis == 0 的个数判断是否等于图的点数,从而判断是否能生成最小生成树。
Ⅷ,用邻接矩阵时要把矩阵初始化为 inf,表示所有点之间皆不可达,不能初始化为 0
1,链式前向星
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<string.h> #define inf 0x3f3f3f3f #define N 110 struct Chain_forward_star // 链式前向星 { int last[N], tot; struct Edge { int pre, to, w; }e[N*N]; void init() { tot = 0; memset(last, -1, sizeof(last)); } void add(int from, int to, int w) { tot++; e[tot].to = to; e[tot].w = w; e[tot].pre = last[from]; last[from] = tot; } }star; int dis[N], p[N]; int Prim(int n) { // ① 初始化 memset(dis, 0x3f, sizeof(dis)); memset(p, -1, sizeof(p)); int s = 1; dis[s] = 0; p[s] = -2; // ⑤ 迭代循环 int m = n - 1; while (m--) { // ② 更新距离 for (int i = star.last[s]; ~i; i = star.e[i].pre) { int to = star.e[i].to, w = star.e[i].w; if (dis[to] > 0 && w < dis[to]) { dis[to] = w; //④ 更新 M:加边入TE p[to] = s; } } // ③ 找最短边:(s, p[s]) int min = inf; for (int i = 1; i <= n; i++) { if (dis[i] && dis[i] < min) { min = dis[i]; s = i; } } // ④ 更新 M:加点入 U dis[s] = 0; // ⑥ 判断:如果找不到最短边 说明不是连通图 if (min == inf) return 0; } // ⑥ 判断:如果全部找到,说明可以生成最小生成树 return 1; } int main(void) { int n, m; // m 是边数, n 是点数 while (scanf("%d%d", &n, &m) != EOF) { star.init(); for (int i = 0; i < m; i++) { int u, v, w; scanf("%d%d%d", &u, &v, &w); star.add(u, v, w); if (u != v) star.add(v, u, w); } Prim(n); // 输出最小生成树 printf("Prim: \n"); for (int i = 1; i <= n; i++) { if (p[i] == -2) continue; printf("%d %d\n", i, p[i]); // 链式前向星找一条边很麻烦 } } return 0; }
2,邻接表 + 优先队列
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<queue> #include<string.h> using namespace std; #define inf 0x3f3f3f3f #define N 110 struct Node { int id; // 当前顶点的id int dis; // 当前顶点到 集合U 的最短距离 Node(int a, int b) :id(a), dis(b) {} // 构造函数 friend bool operator <(const Node &a, const Node &b) { return a.dis > b.dis; } }; vector<Node>v[N]; // 邻接表哦 int dis[N], p[N]; int Prim(int n) { // ① 初始化 memset(dis, 0x3f, sizeof(dis)); memset(p, -2, sizeof(p)); priority_queue<Node>q; int s = 1; p[s] = -2; dis[s] = 0; q.push(Node(s, inf)); // ⑤ 迭代循环 while (q.size()) { Node vertex = q.top(); q.pop(); // ④ 更新M dis[vertex.id] = 0; // ② 更新距离 for (int i = 0; i < v[vertex.id].size(); i++) { Node next = { v[vertex.id][i].id, v[vertex.id][i].dis }; if (dis[next.id] && next.dis < dis[next.id]) { dis[next.id] = next.dis; q.push(next); // ④ 更新M p[next.id] = vertex.id; } } } // ⑥ 判断 for (int i = 1; i <= n; i++) if (dis[i] != 0) return 0; return 1; } int main(void) { int n, m;; while (scanf("%d%d", &n, &m) != EOF) { // 清空邻接表 for (int i = 0; i <= n; i++) v[i].clear(); // 存图 for (int i = 0; i < m; i++) { int x, y, w; scanf("%d%d%d", &x, &y, &w); v[x].push_back(Node(y, w)); if (x != y) v[y].push_back(Node(x, w)); } Prim(n); // 输出最小生成树 printf("Prim: \n"); for (int i = 1; i <= n; i++) { if (p[i] == -2) continue; printf("%d %d\n", i, p[i]); } } }
3,邻接矩阵
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<string.h> #define N 110 #define inf 0x3f3f3f3f int a[N][N]; int dis[N], p[N]; void Prim(int n) { memset(dis, 0x3f, sizeof(dis)); memset(p, -1, sizeof(p)); int s = 1; dis[s] = 0; p[s] = -2; int m = n - 1; while (m--) { for (int i = 1; i <= n; i++) { if (dis[i] && a[s][i] < dis[i]) { dis[i] = a[s][i]; p[i] = s; } } int min = inf; for (int i = 1; i <= n; i++) { if (dis[i] && dis[i] < min) { min = dis[i]; s = i; } } dis[s] = 0; } printf("Prim: \n"); for (int i = 1; i <= n; i++) { if (p[i] == -2) continue; printf("%d %d %d\n", i, p[i], a[i][p[i]]); } } int main(void) { int n, m; while (scanf("%d%d", &n, &m) != EOF) { memset(a, 0x3f, sizeof(a)); for (int i = 0; i < m; i++) { int u, v, w; scanf("%d%d%d", &u, &v, &w); a[u][v] = a[v][u] = w; } Prim(n); } } /* 输入1: 4 4 1 2 1 1 3 4 2 4 1 3 4 3 输出2: 2 1 1 3 4 3 4 2 1 输入2: 6 10 1 2 6 1 4 5 1 3 1 2 3 5 2 5 3 3 5 6 3 6 4 3 4 5 4 6 2 5 6 6 输出2: 2 3 5 3 1 1 4 6 2 5 2 3 6 3 4 */
三,Kruskal
1,算法思想
通过选择边来构建最小生成树。首先根据边权从小到大对边进行循环。然后根据当前边会不会构成环进行选择。
2,步骤
设 N = {V,E} 是一个连通网,M = {U,TE} 是 N 上的一棵最小生成树。
① 初始化:令 U = V,TE = {}
② 排序:将所有的边按照权值 由小到大 排序。
③ 选择:将排序好的所有边依次加入生成树中,会形成环的跳过。(用并查集)
④ 判断:判断最后会不会形成最小生成树。(即 最小生成树的边数 是否等于 该图的点数-1 ) (或者 最小生成树的点数 是否等于 该图的点数)
注意:最小生成树的点数可以利用并查集计算。
3,代码
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<algorithm> using namespace std; #define N 110 int p[N], num[N]; struct edge // 边集 { int u, v; // 起点 终点 int w; // 权重 }e[5000]; int cmp(edge &a, edge &b) // 好像有取值符会更快 { return a.w < b.w; } int find(int x) { if (p[x] != x) p[x] = find(p[x]); return p[x]; } int join(int x, int y) { x = find(x), y = find(y); if (x == y) return 0; p[x] = y; num[y] += num[x]; return 1; } int main(void) { int n, m; // 点数 边数 while (scanf("%d%d", &n, &m), n) { for (int i = 1; i <= n; i++) { p[i] = i; num[i] = 1; } // ② 排序 for (int i = 1; i <= m; i++) scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].w); sort(e + 1, e + m + 1, cmp); //③ 选择 int sum = 0; // 代价 for (int i = 1; i <= m; i++) if (join(e[i].u, e[i].v) == 1) sum += e[i].w; // ④ 判断 if (num[find(1)] == n) printf("%d\n", sum); else puts("?"); } return 0; } /* 测试数据: 6 10 1 2 6 1 4 5 1 3 1 2 3 5 3 4 5 2 5 3 3 5 6 3 6 4 4 6 2 5 6 5 答案: 15 */ /* 下面这种计算边数的方法,只适用于连通图,如果存在多个连通分量,它会计算到别的连通分量的边数 int sum = 0, t = 0; // 代价 边数 for (int i = 1; i <= n; i++) //② 选择 if (join(e[i].u, e[i].v) == 1) { sum += e[i].w; t++; } if (t == m - 1) // ③ 判断 printf("%d\n", sum); else puts("?"); */
四,prim 和 Kruskal 时间复杂度比较
Prim:通过 点 去选择,时间复杂度O( n 的平方)( n 为顶点数),适合边多点少的稠密图
Kruskal:通过 边 去选择,时间复杂度O( eloge )( e 为边数 ),适合边少点多的稀疏图
五,Dijkstra 与 Prim 比较
相同点:
Dijkstra 和 Prim 都是从一个源点开始,不断在剩下的点中选取满足一定条件的点,加入自身,并以此将图的点集分成两个点集。
其中,对于“一定条件”也都是寻求两个集合的某种距离关系,并将距离最短的点加入源点归属的集合中。
差别点:
Dijkstra 的结果是 —— 最短路径树;
Prim 的结果是 —— 最小生成树。
Dijkstra 选择的条件是 —— V-S 中的点到 s 的所有借助 S 最短路径中的最短路径中连通 V-S 和 S 的边;
Prim 选择的条件是 —— V-U 中的点到 U 的所有最短直达边中边长最短的边。
========= ======== ======= ======= ====== ===== ==== === == =
所以我时常害怕,愿中国青年都摆脱冷气,只是向上走,不必听自暴自弃者流的话。能做事的做事,能发声的发声。有一分热,发一分光,就令萤火一般,也可以在黑暗里发一点光,不必等候炬火。
此后如竟没有炬火:我便是唯一的光。倘若有了炬火,出了太阳,我们自然心悦诚服的消失,不但毫无不平,而且还要随喜赞美这炬火或太阳;因为他照了人类,连我都在内。
——《热风·随感录四十一》 鲁迅