生成树
生成树是连通图的最小连通子图。所谓最小是指:若在树中任意增加一条边,则将出现一个回路;若去掉一条边,将会使之变成非连通图。按照该定义,n个顶点的连通网络的生成树有n个顶点,n-1条边。
可知用不同的遍历图的方法,可以得到不同的生成树;从不同的顶点出发,也可能得到不同的生成树。
最小生成树
生成树各边的权值总和称为生成树的权,权最小的生成树称为最小生成树。
最小生成树的概念可以应用到许多实际问题中。例如:以尽可能低的总造价建设城市间的通讯网络,以把10个城市联系在一起。在这10个城市中,任意两个城市间都可以建造通讯线路,通讯线路的造价依据城市间的距离不同而有不同的造价,可以构造一个通讯线路造价网络,在网络中,每个顶点表示城市,顶点之间的边表示城市之间可构造通讯线路,每条边的权值表示该条通讯线路的造价,要想使总的造价最低,实际上就是寻找该网络的最小生成树。
常见的构造最小生成树的方法有Prim算法和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;
}
此时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中。
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为点数。
调用两次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;
}