算法思想 来自:https://www.bilibili.com/video/BV1Ut411976J?from=search&seid=6406725088142163166
一,MST 性质 (Minimum Spaning Tree)
应该知道 最小生成树 并不唯一。 (•‾̑⌣‾̑•)✧˖°
定义:设 N = (V,E )是一个连通网,U 是顶点集V的一个非空子集。
若(u , v )是一条具有最小权值(代价)的边,其中u∈U, v∈V - U,则必存在一棵包含边(u,v)的最小生成树。
这是构造最小生成树的基本性质。
二,Prim 算法
① 算法思想
利用 MST 性质,将图中的点分为两个集合:
U:已落在生成树上的顶点集、
U-V:未落在生成树上的顶点集
接下来就在所有 连通U中的顶点 和 V-U中的顶点 的边中选取 权值最小的边
(解释:U 是未完成的最小生成树,要想所有点连接,则最小生成树必存在边 (U 中一点,V-U 中一点),所以他们中最小的一定在树中)
② 步骤
设 N = (V,E )是一个连通网,TE 是 N 上最小生成树中 边的集合
1,初始化 :令 U={ u0 },TE={ }
2,选择:在所有 u 属于 U,v 属于 V-U 的边(u,v)中,找一条代价最小的边(u0,v0)
3,更新:将(u0,v0)并入集合 TE 中,同时 v0 并入 U
4,重复 2,3 操作,直至 U=V,则 T = { V,TE } 为 N 的最小生成树
③ 例题
步骤:其中 星号:表示该边已经加入最生,打勾:表示在本次比较中找到的 要加入最生的
④ 代码
用链式前向星
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<string.h> #define inf 0x3f3f3f3f #define N 10010 int last[N], tot; struct edge { int to; int w; int pre; }e[N]; void init() { memset(last, -1, sizeof(last)); tot = 0; } 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; } int vis[N]; //(vis[i]==1)为 集合 U ,(vis[i]==0)为 集合 V-U int dis[N]; // 记录 U 集合中的点 到 V-U 的所有点的 最短距离. 如 dis[2]=8 代表所有在 U 中的点 到 点2 的最短距离是 8,这里边的另外一点为 p[2] int p[N]; int Prim(int n) { // 1,初始化 :令 U={ u0 },TE={ },用 p[]记录路径, 这里没用 for (int i = 1; i <= n; i++) { vis[i] = 0; dis[i] = inf; // 既然是要记录最短的距离,则初始化 为 inf p[i] = 0; } int s = 1; // 开始的结点 u0 vis[s] = 1; // U={ u0 } p[s] = -1; // 标记 根 // 4,重复 2,3 操作,直至 U=V,则 T = { V,TE } 为 N 的最小生成树 int sum = 0; // 记录总代价,可以看作是 TE int m = n - 1; // 最小生成树的边数 while (m--) // 最小生成树的边数为原来图的点数减一,即 U == V, { for (int i = last[s]; ~i; i = e[i].pre) // 2,选择:在所有 u 属于 U,v 属于 V-U 的边(u,v)中,找一条代价最小的边(u0,v0) { int to = e[i].to; int w = e[i].w; if (!vis[to] && dis[to] > w) // 更新 U 集合中的点 到 V-U 的所有点的 最短距离 如果这条路走到尽头, 下面还是会选出 最短的那条路径,直接跳过去 { dis[to] = w; p[to] = s; // 记录路径 } } int min = inf; for (int j = 1; j <= n; j++) // 找到 在所有 u 属于 U,v 属于 V-U 的边(u,v)中,代价最小的边( p[j] ,j) { if (!vis[j] && dis[j] < min) { min = dis[j]; s = j; } } if (min == inf) // 如果没有这条边 说明不是 连通图 return 0; // 3,更新:将(u0,v0)并入集合 TE 中,同时 v0 并入 U; vis[s] = 1; // v0 并入 U sum += min; // (u0,v0)并入集合 TE 中 } return sum; } int main(void) { int n, m; // m 是边数, n 是点数 while (scanf("%d%d", &m, &n) == 2 && m) { init(); for (int i = 0; i < m; i++) { int u, v, w; scanf("%d%d%d", &u, &v, &w); add(u, v, w); if (u != v) add(v, u, w); } int sum = Prim(n); if (sum == 0) puts("?"); else printf("%d ", sum); } return 0; } /* 测试数据;长方形 4 4 1 2 1 1 3 4 2 4 1 3 4 4 */
用优先队列
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<queue> #include<string.h> using namespace std; #define N 105 struct Edge { int pre, to, w; }e[N]; int last[N], vis[N], p[N]; int tot = 0, n, m; // m 是边数, n 是点数 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; } void init() { memset(last, -1, sizeof(last)); memset(vis, 0, sizeof(vis)); memset(p, 0, sizeof(p)); tot = 0; } struct Node { int to; int cost; int from; /* 因为 找 最小的边 在队列里面完成了,只有出队列后,我们才知道是哪条边,不过那时候我们也不知道这条边的 起点 是哪一个 所以为了可以记录路径,在这里增加一个变量,记录 边的起点 */ Node() {} Node(int tt, int cc,int qq) :to(tt), cost(cc), from(qq){} friend bool operator < (const Node &a, const Node &b) { return a.cost > b.cost; } }; void prim() { int cnt = 0, ans = 0; priority_queue<Node>q; q.push(Node(1, 0, -1)); p[1] = -1; // 标记 根结点 while (q.size()) // BFS { Node vertex = q.top(); q.pop(); if (vis[vertex.to] == 0) { vis[vertex.to] = 1; ans += vertex.cost; p[vertex.to] = vertex.from; cnt++; for (int i = last[vertex.to]; ~i; i = e[i].pre) // 起点在 点集 T 中,终点在 E-T 中 { Node next = { e[i].to,e[i].w ,vertex.to }; if (vis[next.to] == 0) { q.push(next); } } } } if (cnt != n) puts("?"); else printf("%d ", ans); } int main(void) { while (scanf("%d%d", &m, &n) == 2 && m) { init(); for (int i = 0; i < m; i++) { int u, v, w; scanf("%d%d%d", &u, &v, &w); add(u, v, w); if (u != v) add(v, u, w); } prim(); } return 0; }
用 邻接矩阵
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<string.h> #define inf 0x3f3f3f3f #define N 105 int a[N][N], vis[N], dis[N], p[N]; int n, m; // m 是边数, n 是点数 void init() { memset(vis, 0, sizeof(vis)); memset(dis, 0x3f, sizeof(dis)); memset(a, 0x3f, sizeof(a)); memset(p, 0, sizeof(p)); } int prim() { int s = 1; dis[1] = 0, vis[s] = 1, p[s] = -1; int sum = 0; for (int i = 1; i < n; i++) // 边数 为点数减一 { for (int j = 1; j <= n; j++) // 更新距离,标记起点 { if (vis[j] == 0 && dis[j] > a[s][j]) { dis[j] = a[s][j]; p[j] = s; } } int min = inf; for (int j = 1; j <= n; j++) // 找到 最小的那条边 { if (vis[j] == 0 && dis[j] < min) { min = dis[j]; s = j; } } if (min == inf) return 0; vis[s] = 1; sum += min; } return sum; } int main(void) { while (scanf("%d%d", &m, &n) == 2 && m) { init(); for (int i = 1; i <= m; i++) { int u, v, w; scanf("%d%d%d", &u, &v, &w); a[u][v] = a[v][u] = w; } int sum = prim(); if (sum == 0) puts("?"); else printf("%d ", sum); } system("pause"); return 0; }
在这里提醒一句:注意起点的判断,虽然这边都可以省略这一步。
三,Kruskal
① 算法思想
设 连通网 N = {V,E }, 令 最小生成树 初始状态为 只有 n 个顶点 而 没有边 的非连通图 T = {V,{ }},
每个顶点自成一个连通分量 ,
然后在 E 中,选取 代价最小 的边,若 该边的顶点 落在 T 中的不同的连通分量上(即不能形成 环)
则将该边加入到 T 中,否则,舍去该边,选择下一条边。
依次类推,直至 T 中的所有顶点 都在 同一个连通分量上。
② 步骤
顶点数可以直接用 cnt 数,所以可以不用初始化处理。
1,排序 将所有的边 按照权值 由小到大 排序
2,选择 将排序好的所有边依次加入生成树中,会形成 环 的跳过(用并查集)
3,判断 判断最后会不会形成 最小生成树(即 最小生成树 的边数 是否是 该图 的点数-1 )
③ 例题
④ 代码
#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 from, to; int w; }e[5000]; int cmp(edge &a, edge &b) // 好像有取值 会更快 { return a.w < b.w; } int find(int x) { int a = x; while (x != p[x]) x = p[x]; while (a != x) { int t = p[a]; p[a] = x; a = t; } return 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; // 边数 int m; // 点数 while (scanf("%d%d", &n, &m), n) { for (int i = 1; i <= n; i++) scanf("%d%d%d", &e[i].from, &e[i].to, &e[i].w); for (int i = 1; i <= m; i++) { p[i] = i; num[i] = 1; } sort(e + 1, e + n + 1, cmp); // 1,排序 将所有的边 按照权值 由小到大 排序 int sum = 0; // 算 最小生成树 的代价 int t = 0; // 数 最小生成树 的边数 for (int i = 1; i <= n; i++) // 2,选择 将排序好的所有边依次加入生成树中,会形成 环 的跳过(用并查集判断) { if (join(e[i].from, e[i].to) == 1) // if 不加大括号,只读取一句,即使你写同一行,后面的语句也不属于 if { sum += e[i].w; t++; } } if (num[find(1)] == m) // 3,判断 生成树的边数 是否 等于 点数 //if (t == m - 1) printf("%d ", sum); else puts("?"); } system("pause"); return 0; } /* 测试数据: 10 6 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 */
四,prim 和 Kruskal 比较 (҂‾ ▵‾)︻デ═一 - - - - - -
Prim : 通过 点 去选择 O( n 的平方 )(n 为顶点数) 适合稠密图
Kruskal:通过 边 去选择 O( eloge ) ( e 为边数 ) 适合稀疏图
========= ======== ======= ======= ====== ===== ==== === == =
所以我时常害怕,愿中国青年都摆脱冷气,只是向上走,不必听自暴自弃者流的话。
能做事的做事,能发声的发声。有一分热,发一分光,就令萤火一般,也可以在黑暗里发一点光,不必等候炬火。
此后如竟没有炬火:我便是唯一的光。倘若有了炬火,出了太阳,我们自然心悦诚服的消失,不但毫无不平,
而且还要随喜赞美这炬火或太阳;因为他照了人类,连我都在内。
——《热风·随感录四十一》 鲁迅