【码极客精讲】最小生成树

一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边。最小生成树可以用kruskal(克鲁斯卡尔)算法或prim(普里姆)算法求出。

在一给定的无向图G = (V, E) 中,(u, v) 代表连接顶点 u 与顶点 v 的边(即),而 w(u, v) 代表此边的权重,若存在 T 为 E 的子集且为无循环图,使得联通所有结点的的 w(T) 最小,则此 T 为 G 的最小生成树。

最小生成树其实是最小权重生成树的简称。

生成树和最小生成树有许多重要的应用。

例如:要在n个城市之间铺设光缆,主要目标是要使这 n 个城市的任意两个之间都可以通信,但铺设光缆的费用很高,且各个城市之间铺设光缆的费用不同,因此另一个目标是要使铺设光缆的总费用最低。这就需要找到带权的最小生成树。

最小生成树性质:设G=(V,E)是一个连通网络,U是顶点集V的一个非空真子集。若(u,v)是G中一条“一个端点在U中(例如:u∈U),另一个端点不在U中的边(例如:v∈V-U),且(u,v)具有最小权值,则一定存在G的一棵最小生成树包括此边(u,v)。

为方便说明,先作以下约定:

①将集合U中的顶点看作是红色顶点,②而V-U中的顶点看作是蓝色顶点,③连接红点和蓝点的边看作是紫色边,④权最小的紫边称为轻边(即权重最"轻"的边)。于是,MST性质中所述的边(u,v)就可简称为轻边。

用反证法证明MST性质:

假设G中任何一棵MST都不含轻边(u,v)。则若T为G的任意一棵MST,那么它不含此轻边。

根据树的定义,则T中必有一条从红点u到蓝点v的路径P,且P上必有一条紫边(u',v')连接红点集和蓝点集,否则u和v不连通。当把轻边(u,v)加入树T时,该轻边和P必构成了一个回路。删去紫边(u',v')后回路亦消除,由此可得另一生成树T'。

T'和T的差别仅在于T'用轻边(u,v)取代了T中权重可能更大的紫边(u',v')。因为w(u,v)≤w(u',v'),所以

w(T')=w(T)+w(u,v)-w(u',v')≤w(T)

即T'是一棵比T更优的MST,所以T不是G的MST,这与假设矛盾。

所以,MST性质成立。 

求MST的一般算法可描述为:针对图G,从空树T开始,往集合T中逐条选择并加入n-1条安全边(u,v),最终生成一棵含n-1条边的MST。

当一条边(u,v)加入T时,必须保证T∪{(u,v)}仍是MST的子集,我们将这样的边称为T的安全边。

Prim算法简述

1).输入:一个加权连通图,其中顶点集合为V,边集合为E;

2).初始化:Vnew= {x},其中x为集合V中的任一节点(起始点),Enew= {},为空;

3).重复下列操作,直到Vnew= V:

a.在集合E中选取权值最小的边<u, v>,其中u为集合Vnew中的元素,而v不在Vnew集合当中,并且v∈V(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一);

b.将v加入集合Vnew中,将<u, v>边加入集合Enew中;

4).输出:使用集合Vnew和Enew来描述所得到的最小生成树。

Kruskal算法简述

假设 WN=(V,{E}) 是一个含有 n 个顶点的连通网,则按照克鲁斯卡尔算法构造最小生成树的过程为:先构造一个只含 n 个顶点,而边集为空的子图,若将该子图中各个顶点看成是各棵树上的根结点,则它是一个含有 n 棵树的一个森林。之后,从网的边集 E 中选取一条权值最小的边,若该条边的两个顶点分属不同的树,则将其加入子图,也就是说,将这两个顶点分别所在的两棵树合成一棵树;反之,若该条边的两个顶点已落在同一棵树上,则不可取,而应该取下一条权值最小的边再试之。依次类推,直至森林中只有一棵树,也即子图中含有 n-1条边为止。

伪代码

GenerieMST(G){//求G的某棵MST

T〈-¢; //T初始为空,是指顶点集和边集均空

while T未形成G的生成树 do{

找出T的一条安全边(u,v);//即T∪{(u,v)}仍为MST的子集

T=T∪{(u,v)}; //加入安全边,扩充T

}

return T; //T为生成树且是G的一棵MST

}

注意:

下面给出的两种求MST的算法均是对上述的一般算法的求精,两算法的区别仅在于求安全边的方法不同。

为简单起见,下面用序号0,1,…,n-1来表示顶点集,即是:

V(G)={0,1,…,n-1},

G中边上的权解释为长度,并设T=(U,TE)。

C语言代码

#include<stdio.h>
 #include<stdlib.h>
 #include<iostream.h>
 #defineMAX_VERTEX_NUM20
 #defineOK1
 #defineERROR0
 #defineMAX1000
 typedefstructArcell
 {
 doubleadj;
 }Arcell,AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];
 typedefstruct
 {
 charvexs[MAX_VERTEX_NUM];//节点数组
 AdjMatrixarcs;//邻接矩阵
 intvexnum,arcnum;//图的当前节点数和弧数
 }MGraph;
 typedefstructPnode//用于普利姆算法
 {
 charadjvex;//节点
 doublelowcost;//权值
 }Pnode,Closedge[MAX_VERTEX_NUM];//记录顶点集U到V-U的代价最小的边的辅助数组定义
 typedefstructKnode//用于克鲁斯卡尔算法中存储一条边及其对应的2个节点
 {
 charch1;//节点1
 charch2;//节点2
 doublevalue;//权值
 }Knode,Dgevalue[MAX_VERTEX_NUM];
 
 
//-------------------------------------------------------------------------------
 intCreateUDG(MGraph&G,Dgevalue&dgevalue);
 intLocateVex(MGraphG,charch);
 intMinimum(MGraphG,Closedgeclosedge);
 voidMiniSpanTree_PRIM(MGraphG,charu);
 voidSortdge(Dgevalue&dgevalue,MGraphG);
 
 
//-------------------------------------------------------------------------------
 intCreateUDG(MGraph&G,Dgevalue&dgevalue)//构造无向加权图的邻接矩阵
 {
 inti,j,k;
 cout<<"请输入图中节点个数和边/弧的条数:";
 cin>>G.vexnum>>G.arcnum;
 cout<<"请输入节点:";
 for(i=0;i<G.vexnum;++i)
 cin>>G.vexs[i];
 for(i=0;i<G.vexnum;++i)//初始化数组
 {
 for(j=0;j<G.vexnum;++j)
 {
 G.arcs[i][j].adj=MAX;
 }
 }
 cout<<"请输入一条边依附的定点及边的权值:"<<endl;
 for(k=0;k<G.arcnum;++k)
 {
 cin>>dgevalue[k].ch1>>dgevalue[k].ch2>>dgevalue[k].value;
 i=LocateVex(G,dgevalue[k].ch1);
 j=LocateVex(G,dgevalue[k].ch2);
 G.arcs[i][j].adj=dgevalue[k].value;
 G.arcs[j][i].adj=G.arcs[i][j].adj;
 }
 returnOK;
 }
 intLocateVex(MGraphG,charch)//确定节点ch在图G.vexs中的位置
 {
 inta;
 for(inti=0;i<G.vexnum;i++)
 {
 if(G.vexs[i]==ch)
 a=i;
 }
 returna;
 }
 voidMiniSpanTree_PRIM(MGraphG,charu)//普利姆算法求最小生成树
 {
 inti,j,k;
 Closedgeclosedge;
 k=LocateVex(G,u);
 for(j=0;j<G.vexnum;j++)
 {
 if(j!=k)
 {
 closedge[j].adjvex=u;
 closedge[j].lowcost=G.arcs[k][j].adj;
 }
 }
 closedge[k].lowcost=0;
 for(i=1;i<G.vexnum;i++)
 {
 k=Minimum(G,closedge);
 cout<<"("<<closedge[k].adjvex<<","<<G.vexs[k]<<","<<closedge[k].lowcost<<")"<<endl;
 closedge[k].lowcost=0;
 for(j=0;j<G.vexnum;++j)
 {
 if(G.arcs[k][j].adj<closedge[j].lowcost)
 {
 closedge[j].adjvex=G.vexs[k];
 closedge[j].lowcost=G.arcs[k][j].adj;
 }
 }
 }
 }
 intMinimum(MGraphG,Closedgeclosedge)//求closedge中权值最小的边,并返回其顶点在vexs中的位置
 {
 inti,j;
 doublek=1000;
 for(i=0;i<G.vexnum;i++)
 {
 if(closedge[i].lowcost!=0&&closedge[i].lowcost<k)
 {
 k=closedge[i].lowcost;
 j=i;
 }
 }
 returnj;
 }
 voidMiniSpanTree_KRSL(MGraphG,Dgevalue&dgevalue)//克鲁斯卡尔算法求最小生成树
 {
 intp1,p2,i,j;
 intbj[MAX_VERTEX_NUM];//标记数组
 for(i=0;i<G.vexnum;i++)//标记数组初始化
 bj[i]=i;
 Sortdge(dgevalue,G);//将所有权值按从小到大排序
 for(i=0;i<G.arcnum;i++)
 {
 p1=bj[LocateVex(G,dgevalue[i].ch1)];
 p2=bj[LocateVex(G,dgevalue[i].ch2)];
 if(p1!=p2)
 {
 cout<<"("<<dgevalue[i].ch1<<","<<dgevalue[i].ch2<<","<<dgevalue[i].value<<")"<<endl;
 for(j=0;j<G.vexnum;j++)
 {
 if(bj[j]==p2)
 bj[j]=p1;
 }
 }
 }
 }
 voidSortdge(Dgevalue&dgevalue,MGraphG)//对dgevalue中各元素按权值按从小到大排序
 {
 inti,j;
 doubletemp;
 charch1,ch2;
 for(i=0;i<G.arcnum;i++)
 {
 for(j=i;j<G.arcnum;j++)
 {
 if(dgevalue[i].value>dgevalue[j].value)
 {
 temp=dgevalue[i].value;
 dgevalue[i].value=dgevalue[j].value;
 dgevalue[j].value=temp;
 ch1=dgevalue[i].ch1;
 dgevalue[i].ch1=dgevalue[j].ch1;
 dgevalue[j].ch1=ch1;
 ch2=dgevalue[i].ch2;
 dgevalue[i].ch2=dgevalue[j].ch2;
 dgevalue[j].ch2=ch2;
 }
 }
 }
 }
 intmain()
 {
 inti,j;
 MGraphG;
 charu;
 Dgevaluedgevalue;
 CreateUDG(G,dgevalue);
 cout<<"图的邻接矩阵为:"<<endl;
 for(i=0;i<G.vexnum;i++)
 {
 for(j=0;j<G.vexnum;j++)
 cout<<G.arcs[i][j].adj<<"";
 cout<<endl;
 }
 cout<<"=============普利姆算法===============\n";
 cout<<"请输入起始点:";
 cin>>u;
 cout<<"构成最小代价生成树的边集为:\n";
 MiniSpanTree_PRIM(G,u);
 cout<<"============克鲁斯科尔算法=============\n";
 cout<<"构成最小代价生成树的边集为:\n";
 MiniSpanTree_KRSL(G,dgevalue);
return0;
 }

Kruskal算法 - pascal语言

program didi;

var

a:array[0..100000] of record

s,t,len:longint;

end;

fa,r:array[0..10000] of longint;

n,i,j,x,y,z:longint;

tot,ans:longint;

count,xx:longint;

procedure quick(l,r:longint);

var

i,j,x,y,t:longint;

begin

i:=l; j:=r;

x:=a[(l+r) div 2].len;

repeat

while x>a[i].len do inc(i);

while x<a[j].len do dec(j);

if i<=j then

begin

y:=a[i]; a[i]:=a[j]; a[j]:=y;

inc(i);dec(j);

end;

until i>j;

if i<r then quick(i,r);

if l<j then quick(l,j);

end;

function find(x:longint):longint;

begin

if fa[x]=x then exit(x);

fa[x]:=find(fa[x]);{路径压缩}

exit(fa[x]);

end;

procedure union(x,y:longint);{启发式合并}

var

t:longint;

begin

x:=find(x);

y:=find(y);

if r[x]>r[y] then

begin

t:=x; x:=y; y:=t;

end;

if r[x]=r[y] then inc(r[x]);

fa[x]:=y;

end;

begin

readln(xx,n);

for i:=1 to xx do fa[i]:=i;

for i:=1 to n do

begin

read(x,y,z);

inc(tot);

a[tot].s:=x;

a[tot].t:=y;

a[tot].len:=z;

end;

quick(1,tot);{将边排序}

ans:=0;

count:=0;

i:=0;

while count<=x-1 do{count记录加边的总数}

begin

inc(i);

with a[i] do

if find(s)<>find(t) then

begin

union(s,t);

ans:=ans+len;

inc(count);

end;

end;

write(ans);

end.

Prim算法 - pascal语言

var

m,n:set of 1..100;

s,t,min,x,y,i,j,k,l,sum,p,ii:longint;

a:array[1..100,1..100]of longint;

begin

readln(p);

for ii:=1 to p do

begin

k:=0; sum:=0;

fillchar(a,sizeof(a),255);

readln(x);

m:=[1];

n:=[2..x];

for i:=1 to x do

begin

for j:=1 to x do

begin

read(a[i,j]);

if a[i,j]=0

then a[i,j]:=maxlongint;

end;

readln;

end;

for l:=1 to x-1 do

begin

min:=maxlongint;

for i:=1 to x do

if i in m

then begin

for j:=1 to x do

begin

if (a[i,j]<min)and(j in n)

then begin

min:=a[i,j];

s:=i;

t:=j;

end;

end;

end;

sum:=sum+min;

m:=m+[t];

n:=n-[t];

inc(k);

end;

writeln(sum);

end;

end.

C++模板

//maxe保存了最大边数
structedge
{
intu,v,w;

booloperator<(constedge&b)const
{
returnthis->w>b.w;
}
}e[maxe];

//并查集相关
intf[maxn];

inlinevoidinit()
{
for(inti=0;i<maxn;i++)f[i]=i;
}

intfind(intx)
{
if(f[x]==x)returnx;
elsereturnf[x]=find(f[x]);
}

//主算法
intkruskal(intn,intm)
{
//n:点数,m:边数
//所有边已经预先储存在e数组里
sort(e,e+m);
init();

intans=0;
for(inti=0;i<m;i++)
{
intu=e[i].u,v=e[i].v,w=e[i].w;
if(find(u)==find(v))continue;
f[find(u)]=find(v);
ans+=w;
}

returnans;
}

  • 7
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值