最小生成树——Kruskal算法

【Kruskal算法思想】

1.将图各边按照权值进行排序。

2.将图遍历一次,找出权值最小的边,(条件:此次找出的边不能和已加入最小生成树集合的边构成环),若符合条件,则加入最小生成树的集合中。不符合条件则继续遍历图,寻找下一个最小权值的边。

3.递归重复步骤1,直到找出n-1条边为止(设图有n个结点,则最小生成树的边数应为n-1条),算法结束。得到的就是此图的最小生成树。

【Kruskal算法实现】

第一步:给所有边按照从小到大的顺序排列。

第二步:从小到大依次考察每条边(u,v),这时会出现两种情况。

  1. 如果u和v在同一个连通分量中,那么加入(u,v)后会形成环,因此不能选择;
  2. 如果u和v在不同的连通分量,那么加入(u,v)后一定是最优的。下面给予证明。

反证法:如果不加这条边能得到一个最优解T,则T+(u,v)一定有且只有一个环,而且环中至少有一条边(u',v')的权值大于或等于边(u,v)的权值。删除该边后,得到的新树T' = T+(u,v)-(u',v')不会比T更差。因此,加入(u,v)不会比不加入差。

我的想法:边已经有序排列好了,短的可以选的情况下不选短的???

下面是伪代码:

把所有边排序,记第i小的边为e[i](1<=i<m)
初始化MST为空
初始化连通分量,让每个点自成一个独立的连通分量
for(int i = 0; i < m; i++)
	if(e[i].u和e[i].v不在同一个连通分量){
		把边e[i]加入MST
		合并e[i].u和e[i].v所在的连通分量 
	} 

 在上面的伪代码中,最关键的地方在于“连通分量的查询与合并”:需要知道任意两个点是否在同一连通分量中,还需要合并两个连通分量。

暴力解法:每次“合并”时只在MST中加入一条边(如果使用邻接矩阵,只需G[e[i].u][e[i].v]=1),而“查询”时直接在MST中进行图遍历(DFS和BFS都可以判断连通性)。遗憾的是,这个方法不仅复杂(需要写DFS或者BFS),而且效率不高。

并查集法:把每个连通分量看成一个集合,该集合包含了连通分量中的所有点(这些点两两连通,而具体的连通方式无关紧要,就好比集合中的元素没有先后顺序之分,只有“属于”和“不属于”的区别)。在图中,每个点恰好属于一个连通分量,对应到集合表示中,每个元素恰好属于一个集合。(换句话说,图的所有连通分量可以用若干个不相交集合来表示)

并查集的精妙之处在于:用树来表示集合。例如,包含点1,2,3,4,5,6的图有3个连通分量{1,3}、{2,5,6}、{4},则需要用3棵树来表示。这3棵树的具体形态无关紧要,只要有一棵树包含1、3两个点,一棵树包含2、5、6这3个点,还有一棵树只包含4这一个点即可。规定每棵树的根结点是这棵树所对应集合的代表元。

过程:

把x的父结点保存在pre[x]中,如果x没有父结点,则pre[x] = x 。那么便可以写出“查找结点x所在树的根结点”的递归程序:

int find(int x)
{
	return pre[x] == x ? x : find(pre[x]);  //如果pre[x]等于x,说明x本身就是树根,故返回x;否则返回x的父结点p[x]所在树的树根。
}

需要优化的点:在特殊情况下,这棵树可能是一条长长的链。设链的最后一个结点为x,则每次执行find(x)都会遍历整条链,效率不高。

改进方法:既然每棵树表示的只是一个集合因此树的形态是无关紧要的,并不需要在“查找”操作之后保持树的形态不变,只要顺便把遍历过的结点都改成树根的子结点,下次查找就会快很多了。

于是,Kruskal算法的完整代码便水到渠成的得到:

/*  Kruskal算法构造MST	*/
//第i条边的两个端点序号和权值分别保存在u[i],v[i]和 w[i]中
//排序后第i小的边的序号保存在r[i]中(这叫间接排序:排序的关键字是对象的“代号”,而不是对象本身)
int cmp(const int i, const int j)		//间接排序函数 
{
	return w[i]<w[j];
}
int find(int x)				//并查集的find 
{
	return pre[x] == x ? x : pre[x] = find(pre[x]);    //包含了路径压缩
}
int Kruskal()
{
	int ans = 0;
	for(int i = 0; i < n; i++)	pre[i] = i;		//初始化并查集 
	for(int i = 0; i < m; i++)	r[i] = i;		//初始化边序号 
	sort(r, r+m, cmp);
	for(int i = 0; i < n; i++){
		int e = r[i];
		//找出当前两个端点所在集合编号 
		int x = find(u[e]);
		int y = find(v[e]);
		if(x != y){		//如果在不同集合,合并 
			ans += w[e];
			pre[x] = y;
		}
	}
	return ans;
}

注意,x和y分别是第e条边的两个端点所在连通分量的代表元。合并x和y所在集合可以简单地写成pre[x]=y,即直接把x作为y的子结点,则两个树就合并成一棵树了,注意不能写成pre[u[e]]=pre[v[e]],因为u[e]和v[e]不一定是树根。

并查集效率:在平摊意义下,find函数的时间复杂度几乎可以看成是常数(而union显然是常数时间)。

最后给出完整代码:

#include<iostream>
#include<cstring>
#include<string>
#include<cstdio>
#include<algorithm>
using namespace std;
#define MAX 1000
int father[MAX], son[MAX];
int v, l;

typedef struct Kruskal //存储边的信息
{
    int a;
    int b;
    int value;
};

bool cmp(const Kruskal & a, const Kruskal & b)
{
    return a.value < b.value;
}

int unionsearch(int x) //查找根结点+路径压缩
{
    return x == father[x] ? x : unionsearch(father[x]);
}

bool join(int x, int y) //合并
{
    int root1, root2;
    root1 = unionsearch(x);
    root2 = unionsearch(y);
    if(root1 == root2) //为环
        return false;
    else if(son[root1] >= son[root2])
        {
            father[root2] = root1;
            son[root1] += son[root2];
        }
        else
        {
            father[root1] = root2;
            son[root2] += son[root1];
        }
    return true;
}

int main()
{
    int ncase, ltotal, sum, flag;
    Kruskal edge[MAX];
    scanf("%d", &ncase);
    while(ncase--)
    {
        scanf("%d%d", &v, &l);
        ltotal = 0, sum = 0, flag = 0;
        for(int i = 1; i <= v; ++i) //初始化
        {
            father[i] = i;
            son[i] = 1;
        }
        for(int i = 1; i <= l ; ++i)
        {
            scanf("%d%d%d", &edge[i].a, &edge[i].b, &edge[i].value);
        }
        sort(edge + 1, edge + 1 + l, cmp); //按权值由小到大排序
        for(int i = 1; i <= l; ++i)
        {
            if(join(edge[i].a, edge[i].b))
            {
                ltotal++; //边数加1
                sum += edge[i].value; //记录权值之和
                cout<<edge[i].a<<"->"<<edge[i].b<<endl;
            }
            if(ltotal == v - 1) //最小生成树条件:边数=顶点数-1
            {
                flag = 1;
                break;
            }
        }
        if(flag) printf("%d
", sum);
        else printf("data error.
");
    }
    return 0;
}
原文地址:https://www.cnblogs.com/xzxl/p/7227169.html