最小生成树:Kruskal算法

xiaoxiao2021-02-28  41

最小生成树:Kruskal算法

概览

Kruskal算法(克鲁斯卡尔算法)是一种用来寻找最小生成树的算法,由Joseph Kruskal在1956年发表。Kruskal算法是基于贪心的思想得到的。Kruskal算法在图中存在相同权值的边时有效。

问题

给定一个无向图,如果它任意两个顶点都联通并且是一棵树,那么我们就称之为生成树(Spanning Tree)。如果是带权值的无向图,那么权值之和最小的生成树,我们就称之为最小生成树(MST, Minimum Spanning Tree)。 由Kruskal算法,我们可以求得带权无向图的最小生成树的权值之和。

算法描述

算法思想

Kruskal算法是基于贪心的思想得到的。首先把所有的边按照权值先从小到大排列,接着按照顺序选取每条边,如果这条边的两个端点不属于同一集合,那么就将两个集合合并,直到所有的点都属于同一个集合为止。合并到一个集合时可以使用并查集优化。换言之,Kruskal算法就是基于并查集的贪心算法。

算法过程

记图中有V个顶点,E条边。 将所有边按权值从小到大排序。排序完成后,我们选择边AD,这样我们的图就变成了下图。 在剩下的边里继续寻找权值最小的,这里找到了CE,它的权值也是5。 依此类推找到DF,AB,BE。 下面继续选择到BC或EF。尽管现在长度为8的边是最小的未选择的边,但现在它们已经连通了(对于BC可以通过CE,EB连接,对于EF可以通过EB,BA,AD,DF连接),所以不需要选择BC或EF。类似地,BD也不需要选择。最后剩下EG和FG,选择权值更小的EG。

算法的简单证明

对图的顶点数n做归纳,证明Kruskal算法对任意n阶图适用: 当n=1时,显然能够找到最小生成树。 假设Kruskal算法对n=k阶图适用,那么,在k+1阶图G中,把权值最小的边的两个端点u和v做合并操作,即把u与v合为一个点v',把原来接在u和v的边都接到v'上去,这样就能够得到一个k阶图G',G'的最小生成树T'可以用Kruskal算法得到。 下证T'+{<u,v>}是G的最小生成树。 用反证法,如果T'+{<u,v>}不是最小生成树,设最小生成树是T,即W(T)<W(T'+{<u,v>})。显然T应该包含<u,v>,否则,可以用<u,v>加入到T中,删除环上原有的任意一条边,形成一棵更小权值的生成树,而T-{<u,v>}是G'的生成树(因为G'就是由G中的u,v两点合并产生的),所以W(T')≤W(T-{<u,v>}),即W(T'+{<u,v>})≤W(T)。与W(T)<W(T'+{<u,v>})矛盾。假设不成立。所以T'+{<u,v>}是G的最小生成树,Kruskal算法对k+1阶图也适用。 由数学归纳法,Kruskal算法得证。

程序代码

#include <iostream> #include <algorithm> using namespace std; const int node_num = 100; //最大顶点数 struct Edge { int u, v, w; } e[node_num * node_num]; //边的两端点和权值 bool cmp(const Edge &a, const Edge &b) { return a.w < b.w; } int main() { int v_num; //顶点数 int a_num; //边数 int sum = 0; //生成树权值之和 int vset[node_num]; //记录各顶点所在的树 int k = 1; //记录生成树中顶点数量 cout << "v_num:"; cin >> v_num; cout << "a_num:"; cin >> a_num; for (int i = 0; i < v_num; ++i) { vset[i] = i; //初始化顶点所在的树 } for (int i = 0; i < a_num; ++i) { cin >> e[i].u >> e[i].v >> e[i].w; } sort(e, e + a_num, cmp); //按边权值从小到大排列 for (int index = 0; index < a_num; ++index) { int sn1 = vset[e[index].u], sn2 = vset[e[index].v]; //读取顶点所在的树 if (sn1 != sn2) //如果不在同一个树内 { sum += e[index].w; //加上这条边的权值 ++k; for (int i = 0; i < v_num; ++i) //遍历每个顶点,把属于sn2这棵树的顶点全部加入sn1这棵树 { if (vset[i] == sn2) { vset[i] = sn1; } } } } if (k == v_num) { cout << sum << endl; } else { cout << "E" << endl; //如果k不等于顶点数,说明不存在最小生成树 } return 0; }

运行结果:

算法优化

并查集与路径压缩概述

并查集(Union Find Sets)是一种用于管理分组的数据结构。它具备两个操作:查询元素a和元素b是否为同一组、将元素a和b合并为同一组。 有一组指针指向每个节点的父节点,根节点的指针指向自己,通过循环就可以找到根节点。 路径压缩,就是在每次查找时,找到根节点,然后令查找路径上的每个节点都直接指向根节点。 将a,b节点合并到同一棵树,就是将a节点的根节点的父节点指针指向b节点的根节点。

实现

int find(int x) {//查找根节点 int p = x, t; while (uset[p] != p){ p = uset[p]; }//返回根节点 while (x != p) { t = uset[x]; uset[x] = p; x = t; }//路径压缩 return x; }

例题

USACO agrinet / POJ 1258

描述

农民约翰被选为他们镇的镇长!他其中一个竞选承诺就是在镇上建立起互联网,并连接到所有的农场。当然,他需要你的帮助。 约翰已经给他的农场安排了一条高速的网络线路,他想把这条线路共享给其他农场。为了使花费最少,他想铺设最短的光纤去连接所有的农场。你将得到一份各农场之间连接费用的列表,你必须找出能连接所有农场并所用光纤最短的方案。每两个农场间的距离不会超过100000。

输入输出格式

第一行: 农场的个数,N(3<=N<=100)。 第二行..结尾:接下来的行包含了一个N*N的矩阵,表示每个农场之间的距离。当然,对角线将会是0,因为线路从第i个农场到它本身的距离在本题中没有意义。 只有一个输出,是连接到每个农场的光纤的最小长度和。

样例

SAMPLE INPUT:

4 0 4 9 21 4 0 8 17 9 8 0 16 21 17 16 0

SAMPLE OUTPUT:

28

程序代码

#include <iostream> #include <cstring> #include <algorithm> using namespace std; const int node_num = 100; struct Edge { int u, v, w; } e[node_num * node_num]; bool cmp(const Edge &a, const Edge &b) { return a.w < b.w; } int main() { int v_num; while (cin >> v_num) { int vi = 0, sum = 0, vset[node_num], k = 1; for (int i = 0; i < v_num; ++i) { vset[i] = i; for (int j = 0; j < v_num; ++j) { e[vi].u = i; e[vi].v = j; cin >> e[vi].w; ++vi; } } sort(e, e + vi, cmp); for (int index = 0; index < vi; ++index) { int sn1 = vset[e[index].u], sn2 = vset[e[index].v]; if (sn1 != sn2) { sum += e[index].w; ++k; for (int i = 0; i < v_num; ++i) { if (vset[i] == sn2) { vset[i] = sn1; } } } } cout << sum << endl; } return 0; }

POJ 3723

描述

Windy有一个村庄,他想建立一支军队来保护他的村庄。他选了N个女孩和M个男孩,想让他们做自己的士兵。在没有任何关系的情况下,他雇佣一个士兵需要10000元。有一些女孩和男孩之间存在关系,Windy可以利用这些关系省一些花费。如果女孩x和男孩y间存在关系d且他们中的一个人已经被雇佣了,那么Windy可以用10000-d元雇佣他们中的另一个。现在提供女孩和男孩之间的所有关系,你的任务是算出Windy最少要花多少钱。注意,雇佣一个士兵的时候最多只能用一个关系。

输入输出格式

第1行:测试数据组数。 每组测试数据第1行:3个整数N,M,R。 下面R行,每行包含3个整数xi,yi,di。 在每组测试数据后有一个空行。

1≤N,M≤10000 0≤R≤50000

每组输入数据对应一行输出数据。

样例

SAMPLE INPUT:

2

5 5 8 4 3 6831 1 3 4583 0 0 6592 0 1 3063 3 3 4975 1 3 2049 4 2 2104 2 2 781

5 5 10 2 4 9820 3 2 6236 3 1 8864 2 4 8326 2 0 5156 2 0 1463 4 1 2439 0 4 4373 3 4 8889 2 4 3133

SAMPLE OUTPUT:

71071 54223

程序代码

#include <iostream> #include <cstring> #include <algorithm> #include <cstdio> using namespace std; const int node_num = 20000; int u[50000], v[50000], w[50000], pre[node_num], p[50000]; bool cmp(const int &a, const int &b) { return w[a] > w[b]; } int findroot(const int); int main() { int n; scanf("%d", &n); for (int N = 0; N < n; ++N) { int ngirl, nboy, vi, sum = 0; scanf("%d%d%d", &ngirl, &nboy, &vi); for (int i = 0; i < ngirl + nboy; ++i) { pre[i] = i; } for (int i = 0; i < vi; ++i) { p[i] = i; } for (int i = 0; i < vi; ++i) { int t; scanf("%d%d%d", &u[i], &t, &w[i]); v[i] = t + ngirl; } sort(p, p + vi, cmp); for (int i = 0; i < vi; ++i) { int index = p[i]; int rootx = findroot(u[index]), rooty = findroot(v[index]); if (rootx != rooty) { sum += w[index]; pre[rooty] = rootx; } } sum = (ngirl + nboy) * 10000 - sum; printf("%d\n", sum); } return 0; } int findroot(const int x) { if (pre[x] == x) { return x; } return pre[x] = findroot(pre[x]); }

参考

最小生成树之Kruskal算法最小生成树-Prim算法和Kruskal算法【模版】并查集 及路径压缩
转载请注明原文地址: https://www.6miu.com/read-40457.html

最新回复(0)