Graph 图论
kruskal 最小生成树
相对于prim算法对于图中的点进行处理,kruskal则是对于边进行处理,我们在使用kruskal进行处理的时候,需要用到并查集的思想,后面我们会说。
kruskal算法实现的大致思路是先对边进行从小到大排序,如果这条边的起点和终点已经在一个集合了,说明加上这条边不会增加新的点对于当前这两个点所在的集合,如果这两个点属于同一个集合,那么把这条边连起来之后会让两个集合变成同一个集合(我们的目的不就是把所有点通过边归到一个集合里面吗),重复此操作直到加入集合的点达到|V|个,或者把|E|条边遍历完为止,如果遍历完所有的边不能使所有点归入到一个集合,那么这个图是不连通的。
借用洛谷的一张图:
根据上面的图我们可以知道我们在往图里面加边的时候是根据边的大小把图分成了不同的连通图,直到最后我们把所有的点都连在一起,这也是用了贪心的算法,每次都挑选最短的边,如果这条边不符合要求(所连的两个点本身就在同一个集合里)就不加,最后的结果肯定也是最短的。
判断两个点时候属于一个集合我们可以使用并查集的来实现,并查集是一个什么东西呢?并查集其实就是一个找祖先的过程,定义如果两个点的祖先相同,那么他们属于同一个集合。并查集只需要注意三个地方就可以了:
①数组的初始化:因为刚开始的时候每个点都不连通,所以每个点自己是一个集合,他们的共同祖先都是他们自己
for(int i=1;i<=n;i++){ f[i] = i; }
这一步千万不要漏掉!!!
②我们初始化完成之后我们就开始找祖先了,在这里我们用到一个数组f[maxn],用来存储节点 i 的父节点,这是什么意思呢?想一下你父亲有父亲,你父亲的父亲也有父亲,一直往上找,总归有一个祖先,这样一步步找父亲的过程就实现了找祖先的过程,那么怎样才算是找到呢?我们刚刚初始化的时候是不是把f[i] = i;
,所以当他的父亲是他自己的时候我们的查找也算完成了。
int find(int n){//找节点为n的祖先
while(n!=f[n]){
n = f[n] = f[f[n]];//他爷爷和他父亲和他自己的祖先相同,把他爷爷的爸爸给他,节省后续查找时间,避免树的高度太高
}
return n;
}
③最后一步就是把两个集合归并到一起了,假设A集合的祖先是A,B集合的祖先是B,如果我们想把这两个集合归并到同一个集合,那么实际上也就是让他们的祖先相同,也就是让B的祖先变成A(反过来也可以),也就是f[B] = A;
,这样就完成了两个集合的归并,因为B的子孙往上找找到B的时候还能继续找B的父亲节点,最终找到A,所以这样就实现了两个集合的归并(使两个集合有了相同的祖先)完整代码如下:
#include<iostream>
#include<algorithm>
using namespace std;
int n,m;
int sum = 0;
struct edge{
int from,to;
int cost;
};
bool cmp(edge x,edge y){
return x.cost < y.cost;
}
edge map[500001];
int f[1000001];
int find(int n){
while(n!=f[n]){
n = f[n] = f[f[n]];
}
return n;
}
void kruskal(){
int k = 1;
int cnt = 0;
while(k<n){
edge e = map[cnt++];
int a = find(e.from);
int b = find(e.to);
if(a==b){
continue;
}
else{
f[b] = a;
k++;
sum+=e.cost;
}
}
}
int main(){
cin >> n >> m;
for(int i=1;i<=n;i++){
f[i] = i;
}
for(int i=0;i<m;i++){
edge e;
int a,b,c;
cin >> a >> b >> c;
e.from = a;
e.to = b;
e.cost = c;
map[i] = e;
}
sort(map,map+m,cmp);
kruskal();
cout << sum << endl;
return 0;
}