一、最小生成树
-
是一颗树,无回路,n个顶点的最小生成树有n-1条边
-
是生成树,包含图中的全部顶点,且树的n-1条边都是图中的边
- 该树满足所有顶点都连通且边的权重和最小
如上图,加粗的边即为一颗最小生成树。图中所示的生成树的总权重为37。不过,该最小生成树并不是唯一的,删除边(b, c),然后加入边(a,h),将形成另一棵权重也是37的最小生成树。
二、最小生成树的MST性质
求最小生成树的多数算法都利用了该性质(包括prim算法与kruskal算法)
MST性质:设G=(V,E)是一个连通网络,U是顶点集V的一个真子集。若(u,v)是G中一条“一个端点在U中(例如:u∈U),另一个端点不在U中的边(例如:v∈V-U),且(u,v)具有最小权值,则一定存在G的一棵最小生成树包括此边(u,v)。
可以用反证法证明:假设网G的任何一棵最小生成树都不包含(u,v)。设T是联通网上的一棵最小生成树,当将边(u,v)加入到T中时,有生成树的定义,T中必存在一条包含(u,v)的回路,(因为最小生成树是无向的,所以不用考虑边的方向)。另一方面,由于T是生成树,则T中必存在另一条边(u',v'),其中u'∈U,v'∈V-U,应为生成树中的各个顶点都是连通的,所以必定存在这样一条边使得点集U与V-U连通,且u和u'之间,v和v'之间均有路径相通。删去边(u',v'),便可取消上述回路,同时得到另一棵生成树T' (回路产生的根本原因是因为有两条边(u,v),(u',v')均连通了点集U与V-U,现在删除其中一条边,回路当然就消失了)。应为(u,v)的代价不高于(u',v'),即小于等于(u',v'),则T'的代价亦不高于T,又由于T是最小生成树,所以T'与T的代价相等,故T'为包含了边(u,v)的最小生成树。由此,假设矛盾。ps:这里实在理解不了的话也可以先记住后面的算法形式
三、最小生成树建立的主要思想
贪心思想:
“贪”——解决问题时是一步一步来解决的,而在解决问题的过程中,每一步都要最好的(即只关注于眼前最好的)
“好”——在不同的问题中,“好”有不同的定义,在最小生成树问题中,“好”即为权重最小的边
约束条件:1.只能用图里已有的边 2.只能正好用掉v-1条边 3.不能有回路
这个贪心策略可以由下面的通用方法来表述。该通用方法在每个时刻生长最小生成树的一条边,并在整个策略的实施过程中,管理一个遵守下述循环不变式的边集合A:
在每遍循环之前,A是某棵最小生成树的一个子集。
在每一步,我们要做的事情是选择一条边(u, v), 将其加入到集合A中,使得A不违反循环不变式,即AU{(u,v)} (A中的边并上边(u,v))也是某棵最小生成树的子集。
(想要更深入的了解算法原理的朋友可以去看一看算法导论这本书,不过书上的解释确实难懂,但是我个人觉得算法是一种思想,既然学算法,最好是要弄懂原理)
好了,下面终于要开始写算法了!!!
四、prim算法(适用于稠密图,时间复杂度只与结点数有关,与弧数无关)
假设G=(V,E)是连通网,TE是G上最小生成树中边的集合。算法从U={ u。}(u。∈V,u。是开始算法时任选的点),TE={ }(即TE为空)开始,重复执行下述操作:在所有u∈U,v∈V-U的边
(u,v)∈E中找到一条代价最小的边(u。,v。)并入集合TE,同时v。并入U,直至U=V为止。此时TE中必有n-1条边,则T={V,ET}为G的最小生成树。(U表示已近被确定为最小生成树中顶点的集合,TE表示已近确定为最小生成树中的边的集合)
代码:
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#define MAX_SIZE 50 //最大结点数
#define MMM 66666 //正无穷,当邻接矩阵为该值时则表明两个点之间无直接边相连
typedef int Elemtype;
typedef struct{
int vexsnum;//结点数目
int arcsnum;//弧数
int arcs[MAX_SIZE][MAX_SIZE];//临接矩阵
Elemtype vexs[MAX_SIZE];//结点表
}Mgraph;//无向网结构
void creat_graph(Mgraph* G);//创建无向网
int get_location(Mgraph G,Elemtype v);//获取值为v的结点在结点表中的位置
void prim(Mgraph G,int v);//普里姆算法(从结点v开始生成)
void print_MST(Mgraph G);
int dist[MAX_SIZE];//记录从点集U的点到点集V-U具有最小代价的边(若存在与U中点无直接边相连的点,则dist置为MMM)
int parent[MAX_SIZE];//记录最小生成树结点的双亲
bool inu[MAX_SIZE];//判断结点是否在集合U中
int main()
{
Mgraph G;
creat_graph(&G);
printf("您要以哪个结点为根生成最小生成树:");
Elemtype k;
scanf("%d",&k);
int v=get_location(G,k);
prim(G,v);
print_MST(G);
return 0;
}
void creat_graph(Mgraph* G)
{
int i,j;
printf("请输入无向网的结点数和弧数:");
scanf("%d%d",&(G->vexsnum),&(G->arcsnum));
for(i=0;i<G->vexsnum;i++)//初始化临接矩阵
{
for(j=0;j<G->vexsnum;j++)
G->arcs[i][j]=MMM;
}
printf("请输入各结点的值:");
for(i=0;i<G->vexsnum;i++)
scanf("%d",&(G->vexs[i]));
printf("请输入弧所连接的两结点的值即弧的权重:\n");
Elemtype a,b;
int x,y,weight;
for(i=0;i<G->arcsnum;i++)
{
scanf("%d%d%d",&a,&b,&weight);
x=get_location(*G,a);
y=get_location(*G,b);
G->arcs[x][y]=G->arcs[y][x]=weight;
}
printf("无向网创建完成!\n");
}
int get_location(Mgraph G,Elemtype v)
{
int i;
for(i=0;i<G.vexsnum&&G.vexs[i]!=v;i++);
return i;
}
void prim(Mgraph G,int v)
{
int i,num=0;
for(i=0;i<G.vexsnum;i++)//初始化三个数组
{
dist[i]=MMM;
inu[i]=false;
parent[i]=-1;
}
dist[v]=0;
while(1)
{
int min=-1;
for(i=0;i<G.vexsnum;i++)//寻找未被收录到U且dist最小的点
{
if(min==-1&&dist[i]!=MMM&&inu[i]==false)
min=i;
if(min!=-1&&dist[i]<dist[min]&&inu[i]==false)
min=i;
}
if(min==-1)//未找到这样的点
break;
inu[min]=true;//将该结点收录进U
num++;
for(i=0;i<G.vexsnum;i++)//更新dist
{
if(G.arcs[min][i]!=MMM&&inu[i]==false)//即v与i之间有边且i尚未收录进U
{
if(dist[i]>G.arcs[min][i])
{
dist[i]=G.arcs[min][i];
parent[i]=min;
}
}
}
}
if(num<G.vexsnum)//U中收录的顶点不到G.vexsnum个
{
printf("生成树不存在!\n");
exit(1);
}
}
void print_MST(Mgraph G)
{
printf("最小生成树中的边为:\n");
int i,sum=0;
for(i=0;i<G.vexsnum;i++)
{
if(parent[i]!=-1)
{
printf("%d--%d\n",G.vexs[i],G.vexs[parent[i]]);
sum+=G.arcs[i][parent[i]];
}
}
printf("最小生成树的权重之和为:%d\n",sum);
}
假设我们从第一步开始,将点u1加入U,选出一条代价最小的边(u1,v1),并将结点v1加入U,由MST性质可知存在一颗最小生成树T包含这条边,此时我们选出第二条连通点集U与V-U且代价最小的边(u2,v2),若T包含这条边,则这两条边都为同一棵最小生成树的边(符合我们的目的),若T不包含这条边,则将该边加入进T,则T中必定会形成一个环,且T中存在另一条边(x,y)连通点集U与V-U(x∈U,y∈V-U),又因为(u2,v2)同样连通这两个点集,所以将(x,y)从T中删除后,环消失,得到的新树T1仍为一棵生成树,又因为(u2,v2)为连通U与V-U的最小代价边,所以weight(u2,v2)<=weight(x,y),故T1的所有边权重和<=T的所有边的权重和,由于T为最小生成树,所以只能取等号,所以T1也是最小生成树,所以(u1,v1)与(u2,v2)都包含与最小生成树T1,以此类推,可得,每加入一条新边,都存在一棵最小生成树包含该边和之前生成的所有边,所以按prim算法得到的一定是一棵最小生成树。
五、kruskal算法(适用于稀疏图,时间复杂度只与弧数有关,与结点数无关)
kruskal算法较prim算法来说要容易理解一点
算法思想:将连通分量连成树,假设连通网G=(V,E),则令最小生成树的初始状态为只有n个顶点而无边的非连通图T=(V,∅),图中每个顶点自成一个连通分量。在E中选择代价最小的边,若该边依附的顶点落在不同的连通分量上,则将此边加入到T 中,否则舍去此边而选择下一条代价最小的边。以此类推,直至T中所有顶点都在同一连通分量为止。(因为如果该边的依附顶点都属于同一连通分量的话,将此边加入会在强连通分量中形成环,不符合最小生成树的定义)
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 50
//由于我们每次需要从图中选出一条还未加入最小生成树且符合最小生成树要求的代价最小边,所以
//用什么方法实现这一操作对算法效率的影响很大
//分析可知用最小堆来存储边,用并查集来判断该边所依附的顶点是否属于同一连通分量的算法效率最高
typedef char Elemtype;
struct Heap{
int v1;
int v2;//记录边所依附的两结点的下标
int weight;//记录边的权重
}heap[MAX_SIZE];//堆结构
typedef struct arc{
int index;//记录边所连接的结点下标
int weight;
struct arc* next;
}arc;//边结构
typedef struct{
Elemtype data;//数据域
arc* firstarc;
}vex;//结点结构
typedef struct{
int vexsnum;
int arcsnum;
vex vexs[MAX_SIZE];//结点表
}Mgraph;//图结构
int gather[MAX_SIZE];//并查集
void creat_minheap(int last);//生成最小堆 (last为堆中最后一个结点的编号)
void creat_graph(Mgraph* G);//建立无向网
int get_location(Mgraph G,Elemtype e);//找到顶点e在结点表中的位置
struct Heap get_minheap(int* last);//取出最小堆的顶点
void kruskal(Mgraph G,struct Heap* MST);//克鲁斯卡尔算法
int find_root(int k);//找到并查集中结点编号为k的元素的根
void print_it(Mgraph G,struct Heap* MST);
int main()
{
Mgraph G;
creat_graph(&G);
struct Heap MST[G.vexsnum-1];//记录最小生成树的n-1条边
kruskal(G,MST);
print_it(G,MST);
return 0;
}
void creat_graph(Mgraph* G)
{
printf("请输入有向网的结点数和弧数:");
scanf("%d%d",&(G->vexsnum),&(G->arcsnum));
getchar();
printf("请输入各顶点的值:\n");
int i;
for(i=0;i<G->vexsnum;i++)
{
scanf("%c",&(G->vexs[i].data));
getchar();
G->vexs[i].firstarc=NULL;
}
printf("请输入边所依附的两顶点和边的权重:\n");
Elemtype m,n;
int x,y,weight;
for(i=0;i<G->arcsnum;i++)
{
arc* new_arc1=(arc*)malloc(sizeof(arc));
arc* new_arc2=(arc*)malloc(sizeof(arc));
scanf("%c %c %d",&m,&n,&weight);
getchar();
x=get_location(*G,m);
y=get_location(*G,n);
new_arc1->index=y;
new_arc1->next=G->vexs[x].firstarc;
G->vexs[x].firstarc=new_arc1;
new_arc2->index=x;
new_arc2->next=G->vexs[y].firstarc;
G->vexs[y].firstarc=new_arc2;
new_arc1->weight=new_arc2->weight=weight;
//初始化堆
heap[i].v1=x;
heap[i].v2=y;
heap[i].weight=weight;
}
printf("有向网建立完毕!\n");
}
int get_location(Mgraph G,Elemtype e)
{
int i;
for(i=0;i<G.vexsnum&&G.vexs[i].data!=e;i++);
return i;
}
void creat_minheap(int last)
{
if(last==0)
return ;
int pre,parent,child;
parent=(last-1)/2;
struct Heap H;
while(parent>=0)
{
pre=parent;
child=pre*2+1;
while(child<=last)
{
if(child!=last&&heap[child].weight>heap[child+1].weight)//找到左右孩子中较小的
child++;
if(heap[pre].weight>heap[child].weight)
{
H=heap[pre];
heap[pre]=heap[child];
heap[child]=H;
pre=child;
child=pre*2+1;
}
else
break;
}
parent--;
}
}
struct Heap get_minheap(int* last)
{
struct Heap H=heap[0];
struct Heap E;
int pre=0,child=1;
heap[0]=heap[*last];
(*last)--;
while(child<=*last)
{
if(child!=*last&&heap[child].weight>heap[child+1].weight)
child++;
if(heap[child].weight<heap[pre].weight)
{
E=heap[pre];
heap[pre]=heap[child];
heap[child]=E;
pre=child;
child=pre*2+1;
}
else
break;
}
return H;
}
void kruskal(Mgraph G,struct Heap* MST)
{
int i;
for(i=0;i<G.vexsnum;i++)//初始化并查集
gather[i]=-1;
int last=G.arcsnum-1;
creat_minheap(last);
struct Heap H;
int num=0;
while(num<G.vexsnum-1)
{
H=get_minheap(&last);
int root1=find_root(H.v1);
int root2=find_root(H.v2);
if(root1!=root2)//如果边所依附的顶点不属于同一集合
{
MST[num++]=H;
//将两个集合合并
if(gather[root1]<gather[root2])//说明root1中的元素更多
{
gather[root1]+=gather[root2];
gather[root2]=root1;
}
else
{
gather[root2]+=gather[root1];
gather[root1]=root2;
}
}
if(last<0&&num<G.vexsnum-1)
{
printf("最小生成树不存在!\n");
exit(1);
}
}
}
int find_root(int k)
{
while(gather[k]>=0)
k=gather[k];
return k;
}
void print_it(Mgraph G,struct Heap* MST)
{
printf("该最小生成树的边为:\n");
int sum=0;
int i;
for(i=0;i<G.vexsnum-1;i++)
{
printf("%c--%c\n",G.vexs[MST[i].v1].data,G.vexs[MST[i].v2].data);
sum+=MST[i].weight;
}
printf("最小生成树的权重和为%d\n",sum);
}
这个博客写了两天,不容易呀。。。。。。。。(也可能是我太懒了,边写边玩)