最小生成树-Kruskal算法

生成树

生成树是连通图的最小连通子图。所谓最小是指:若在树中任意增加一条边,则将出现一个回路;若去掉一条边,将会使之变成非连通图。按照该定义,n个顶点的连通网络的生成树有n个顶点,n-1条边

可知用不同的遍历图的方法,可以得到不同的生成树;从不同的顶点出发,也可能得到不同的生成树。




最小生成树

生成树各边的权值总和称为生成树的权,权最小的生成树称为最小生成树

最小生成树的概念可以应用到许多实际问题中。例如:以尽可能低的总造价建设城市间的通讯网络,以把10个城市联系在一起。在这10个城市中,任意两个城市间都可以建造通讯线路,通讯线路的造价依据城市间的距离不同而有不同的造价,可以构造一个通讯线路造价网络,在网络中,每个顶点表示城市,顶点之间的边表示城市之间可构造通讯线路,每条边的权值表示该条通讯线路的造价,要想使总的造价最低,实际上就是寻找该网络的最小生成树。

常见的构造最小生成树的方法有Prim算法和Kruskal算法

下面介绍Kruskal算法




Kruskal算法
Kruskal算法通常以边集数组作为存储结构。

它的做法是

1).记Graph中有v个顶点,e个边

2).新建图Graphnew,Graphnew中拥有原图中相同的e个顶点,但没有边

3).将原图Graph中所有e个边按权值从小到大排序

4).循环:从权值最小的边开始遍历每条边直至图Graph中所有的节点都在同一个连通分量中

                if 这条边连接的两个节点于图Graphnew中不在同一个连通分量中则添加这条边到图Graphnew


可行性证明:

对图的顶点数n做归纳,证明Kruskal算法对任意n阶图适用。

归纳基础

n=1,显然能够找到最小生成树。

归纳过程

假设Kruskal算法对n≤k阶图适用,那么,在k+1阶图G中,我们把最短边的两个端点a和b做一个合并操作,即把u与v合为一个点v',把原来接在u和v的边都接到v'上去,这样就能够得到一个k阶图G'(u,v的合并是k+1少一条边),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'的生成树。所以W(T-{<u,v>})<=W(T'),也就是W(T)<=W(T')+W(<u,v>)=W(T'+{<u,v>}),产生了矛盾。于是假设不成立,T'+{<u,v>}是G的最小生成树,Kruskal算法对k+1阶图也适用。

由数学归纳法,Kruskal算法得证。


下面看具体数据和模拟过程


typedef struct{
	int begin;
	int end;
	int weight;
}Edge;

如上图所示边集数组,权值按由小到大排列。于是Kruskal算法代码如下。其中MAXEDGE为边数量的极大值,此处大于等于9即可。

void MiniSpanTree_Kruskal(MGraph G)
{
	int i,n,m;
	Edge edges[MAXEDGE];//定义边集数组
	int parent[MAXEDGE];//定义一数组用来判断边与边是否形成环路
	//此处省略将邻接矩阵G转化为边集数组edges并按权由小到大排序的代码
	for(i=0;i<G.numVertexes;++i)
		parent[i]=0;//初始化数组值为0
	for(i=0;i<G.numEdges;++i){//循环每一条边 
		n=Find(parent,edges[i].begin);
		m=Find(parent,edges[i].end);
		if(n!=m){//假如n与m不等,说明此边没有与现有生成树形成环路 
			parent[n]=m;//将此边的结尾顶点放入下标为起点的parent中
			//表示此顶点已经在生成树集合中
			printf("(%d,%d) %d",edges[i].begin,edges[i].end,edges[i].weight); 
		}
	} 
}

int Find(int *parent,int f)//查找连线顶点的尾部下标 
{
	while(parent[f]>0)
		f=parent[f];
	return f;
}


1.程序开始运行,第6行省略掉了将邻接矩阵转换为边集数组并按权值从小到大排序的代码。也就是说,在第六行开始,已经有了结构为edge,数据内容是上图中的一维数组edges。

2.第7~8行声明一个数组parent,并将它的值都初始化为0,它的作用后面介绍。

3.第9~17行,对边集数组做循环遍历,开始时,i=0。

4.第10行,调用了第19~25行的函数Find,传入的参数是数组parent和当前权值最小边(V4,V7)的begin:4。因为parent中全都是0所以传出值使得n=4。

5. 第11行,同样做法,传入(V4,V7)的end:7。传出值使得m=7。

6.第12~16行,n与m不相等,因此parent[4]=7。此时parent数组值为{0,0,0,0,7,0,0,0,0},并且打印得到“(4,7) 7”。此时我们已经将边(V4,V7)纳入到最小生成树中,如下图:


7.循环返回,执行10~16行,此时i=1,edge[1]得到边(V2,V8),n=2,m=8,parent[2]=8,打印结果为“(2,8) 8”,此时parent数组值为{0,0,8,0,7,0,0,0,0},表示边(V4,V7)和边(V2,V8)已经纳入到最小生成树,如下图:


8.再次执行10~16行,此时i=2,edge[2]得到边(V0,V1),n=0,m=1,parent[0]=1,打印结果为“(0,1) 10”,此时parent数组值为{1,0,8,0,7,0,0,0,0},此时边(V4,V7)、(V2,V8)和(V0,V1)已经纳入到最小生成树,如下图:


9.当i=3、4、5、6时,分别将(V0,V5)、(V1,V8)、(V3,V7)、(V1,V6)纳入到最小生成树中,如下图:
此时parent数组为{1,5,8,7,7,8,0,0,6}


从i=6图中粗线可以得到,其实是有两个连通的边集合A与B中纳入到最小生成树中的,如下图


当parent[0]=1,表示V0和V1已经在生成树的边集合A中。此时将parent[0]=1的1改为下标,由parent[1]=5,表示V1和V5在边集合A中,parent[5]=8表示V5与V8在边集合A中,parent[8]=6表示V8与V6在边集合A中,parent[6]=0表示集合A暂时到头,此时边集合A有V0、1、5、8、6。接着查看parent中没有查看的值,parent[2]=8表示V2和8在一个集合中,剩下的点同理在另一个集合B中。

10.当i=7时,第10行,调用Find函数,会传入参数edges[7].begin=5。此时第22行,parent[5]=8>0,所以f=8,再循环得parent[8]=6。因parent[6]=0所以Find返回后第10行得到n=6。而此时第11行,传入参数edges[7].end=6得到m=6。此时n=m,不再打印,继续下一循环。因为边(V5,V6)使得边集合A形成了环路,因此不能将它纳入到最小生成树中,如上图所示。

11.当i=8时,与上面相同,由于边(V1,V2)使得边集合A形成了环路,因此不能将它纳入到最小生成树中。

12.当i=9时,边(V6,V7),第10行得到n=6,第11行得到m=7,因此parent[6]=7,打印“(6,7) 19”。此时parent数组值为
{1,5,8,7,7,8,7,0,6},如下图所示:


13.此后循环构造的边均构成环路,最终最小生成树即为上图。


总结
使用邻接矩阵作为存储结构的Prim算法的时间复杂度为O(ElogE),E为边数。这里计算的方法是:
按权值对边排序,快排是O(ElogE)。
初始化所有点各自为一个森林 这一步是O(n)的,n为点数。
然后在这一片森林中添加边,n个点构成的树是有n-1条边,因此需要执行n-1次以下操作:
调用两次Find函数判断是否已经在一个连通分量中,这一步因为这里没有用路径压缩优化,是O(logE)的。
所以总的时间复杂度为:O(ElogE)+O(n)+O((n-1)*(logE)),即O(ElogE)。


路径压缩优化:从已经排序的边序列中,挑选长度最短的,且两端不在同一棵树中的一条边,判断是否是同一棵树是利用并查集进行查询,挑出这一条边之后,把两个端点代表的树合并为一棵,即并查集的合并,这是O(1)的。
算法导论中有证明添加路径压缩以及按秩合并的并查集的复杂度是阿克曼函数的反函数,这是一个增长非常非常快的函数的反函数,这里近似为O(1)了



例:POJ - 2395

求minispan_tree中longest edge

#include<iostream>
#include<algorithm>
#include<cstdio>
using namespace std;
const int maxn=100010,inf=0x3f3f3f3f;

int n,m;
int ans;

struct Edge{
    int from,to,dist;
    Edge() {}
    Edge(int x,int y,int w):from(x),to(y),dist(w){}
    bool operator < (const Edge & e) const {//后面的const必须加上
        return (dist<e.dist);
    }
};

Edge edges[maxn];//这里有Edge型的变量edges  前面Edge结构体必须加上Edge() {}


int father[maxn];

int Find(int x)
{
    return father[x]= father[x]==x? x: Find(father[x]);
}

void kruskal()
{
    sort(edges,edges+m);
    //初始化并查集
    for(int i=1;i<=n;++i) father[i]=i;

    for(int i=0;i<m;++i){
        int fa1=Find(edges[i].from);
        int fa2=Find(edges[i].to);
        if(fa1!=fa2){
            father[fa1]=fa2;
            ans=max(ans,edges[i].dist);
            //printf("(%d,%d) %d\n",edges[i].from,edges[i].to,edges[i].dist);
        }
    }
}

int main()
{
    //ios::sync_with_stdio(false);
    scanf("%d %d",&n,&m);
    ans=-inf;
    for(int i=0;i<m;++i){
        scanf("%d %d %d",&edges[i].from,&edges[i].to,&edges[i].dist);
    }
    kruskal();
    printf("%d\n",ans);
    return 0;
}



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值