1、何为最小生成树(Minimum Spanning Tree)问题?
对于一个有权的联通无向图G=(V,E,ω),我们希望找到一个无环子集T⊆E,将所有节点连接起来并使总权值最低。
由于T是无环的,并且是联通的,所以T必然是一棵树,且是由G生成的生成树。我们将在G中求取这样一个T的问题叫做最小生成树问题。
求取该问题有两种算法:Kruskal算法和Prim算法,这两种算法都是基于贪心算法。
2、最小生成树的证明
①核心思想:每个时刻生长最小生成树的一条边。且满足下列规则:令A为某颗最小生成树的一个子集,每一步中在G中选择一条边(u,v)加入A中,使A∪{(u,v)}仍然为某颗最小生成树的子集。(这样的边称为A的安全边)
②证明所需前置概念:
无向图G=(V,E)的一个切割(S,V-S)是集合V的一个划分。如果一条边(u,v)∈E的一个端点在S、另一个在V-S上,则称该边横跨切割(S,V-S)。对于一个集合A⊆E,如果A中不存在横跨该切割的边,则称该切割尊重集合A。在横跨一个切割的所有边中,权重最小的边称为轻量级边。
③用来辨认安全边的相关定理。
定理一:设G=(V,E)是一个在边E上定义了实数值权重函数ω的连通无向图。设集合A为E的一个子集,且A包括在G的某颗最小生成树中,设(S,V-S)是G中尊重集合A的任意一个切割,又设(u,v)为横跨该切割的一条轻量级边,则边(u,v)对于集合A来说是安全的。
3、Kruskal算法和Prim算法
这两种算法都是采用一条具体的规则来确定2中的安全边。在Kruskal算法中,集合A为一个森林,其结点为给定图的结点,每次加入到A中的安全边永远是权重最小的连接两个不同分量的边。而Prim算法中,集合A为一颗树,每次加入A中的安全边永远是连接A和A之外某个结点的边中权重最小的边 。
Prim算法(基于邻接矩阵构造的图):
void MiniSpanTree_Prim(MGraph G)
{
int min,i,j,k;
int adjvex[MAXVEX];//store the relevant vex
int lowcost[MAXVEX];//store the relevant cost
lowcost[0]=0;
adjvex[0]=0;
for(i=1;i<G.numVertexes;i++)
{
lowcost[i]=G.arc[0][i];
adjvex[i]=0;
}
for(i=1;i<G.numVertexes;i++)
{
min=INFINITY;
j=1;k=0;
while(j<G.numVertexes)
{
if(lowcost[j]!=0&&lowcost[j]<min)
{
min=lowcost[j];
k=j;
}
j++;
}
printf("(%d,%d)",adjvex[k],k);
lowcost[k]=0;
for(j=1;j<G.numVertexes;j++)
{
if(lowcost[j]!=0&&G.arc[k][j]<lowcost[j])
{
lowcost[j]=G.arc[k][j];
adjvex[j]=k;
}
}
}
}
Kruskal算法(基于边集数组构造的图):
typedef struct
{
int begin;
int end;
int weight;
}Edge;
void MiniSpanTree_Kruskal(MGraph G)
{
int i,n,m;
Edge edges[MAXEDGE];
int parent[MAXEDGE];//to check if they cause a circuit
for(i=0;i<G.numVertexes;i++)
parent[i]=0;
for(i=0;i<G.numEdges;i++)
{
n=Find(parent,edges[i].begin);
m=Find(parent,edges[i].end);
if(n!=m)
{
parent[n]=m;
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;
}