zoukankan      html  css  js  c++  java
  • 最小生成树——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;
    }
  • 相关阅读:
    16.检查是否为BST
    15.输出单层结点
    14.高度最小的BST
    *13.有向路径检查
    12.二叉树平衡检查
    11.双栈排序
    10.回文链表
    9.链式A+B
    8.链表分割
    7.访问单个节点的删除
  • 原文地址:https://www.cnblogs.com/xzxl/p/7227169.html
Copyright © 2011-2022 走看看