图的应用—最小生成树
前言
终于迎来了图的应用这部分内容的学习啦,接下来将介绍普里姆算法和克鲁斯卡尔算法。若对文章内容有疑惑,欢迎评论区发言,小白会积极改进的。
参考文献:严蔚敏.吴伟民.数据结构(C语言结构)[M].北京:清华大学出版社,2011.
一、最小生成树
1.什么是最小生成树?
定义:在一个连通网的所有生成树中,各边的代价之和最小的那棵生成树称为该联通网的最小代价生成树,简称为最小生成树。
知识点回顾
1.连通图的生成树
连通图的生成树:一个极小连通子图,它包含图中全部顶点,但只有足以构成一棵树的n-1条边,这样的连通子图称为连通图的生成树。
一棵有n个顶点的生成树有且仅有n-1条边。如果一个图有n个顶点和小于n-1条边,则是非连通图。如果它多于n-1条边,则一定有环。但是,有n-1条边的图不一定是生成树。
连通图的生成树如图示:
2.极大连通子图
子图是无向图的连通子图,将无向图的任何不在该子图中的顶点加入,子图就不再连通。
3.极小连通子图
子图是无向图的连通子图,在该子图中删除任何一条边,子图不在连通。
最小生成树应用举例:在n个城市之间建立通信联络网,在每两个城市之间设置一条线路,我们要付出相应的经济代价,求怎么在城市之间设置线路使得总花费最少?
可以用连通网来表示这n个城市,这n个城市之间最多可能设置n(n - 2)/2条线路。在n个城市之间建立通信联络网,只需要n-1条路就可以联络起来,考虑到总花费要最少,所以我们只需要设置n-1条线路,这样可以对这n个顶点建立许多不同的生成树并且总花费要最少,那么最合理的通信网应该是代价之和最小的生成树。这就需要我们求最小代价生成树,也就是最小生成树。
2.MST的性质
构造最小生成树有多种算法,其中多数算法利用了最小生成树的一种简称为MST的性质:
假设N = (V, E)是一个连通网,U是顶点集V的一个非空子集,若(u,v)是一条具有最小权值(代价)的边,其中u∈U、v∈V - U,则必存在一棵包含边(u, v)的最小生成树。
二、普里姆算法
1.普里姆算法的构造过程
假设N = (V,E)是连通网,TE是N上最小生成树中边的集合。
(1)U = {uo}(uo∈V),TE = {}。
(2)在所有u∈U、v∈V-U的边(u,v)∈E中找一条权值最小的边(uo, vo)并入集合TE,同时vo并入U。
(3)重复(2),直到U = V为止。
简单来说就是,任意选择一个顶点开始,然后挑选这个顶点与其他顶点相连边权值最小的那一条边,之后又找这两个顶点与其他顶点相连边权值最小的那一条边,重复下去,就可以形成一个最小生成树。
2.普里姆算法的实现
假设一个无向网G以邻接矩阵形式存储,从顶点u出发构造G的最小生成树T,要求输出T的各条边。为实现这个算法需附设一个辅助数组closedge,以记录从U到V-U具有最小权值的边。对于每个顶点vi∈V-U,在辅助数组中存在一个相应分量closedge[i - 1]它包括两个域:lowcost和adjvex。其中lowcost存储最小边上的权值,adjvex存储最小边在U中的那个顶点。显然,closedge[i - 1].lowcost = min{cost(ui, vi)},其中cost(u,v)表示赋予边(u, v)的权。
struct {
VerTexType adjvex; // 最小边在U中的那个顶点
ArcType lowcost; // 最小边上的权值
}closedge[MVNum];
3.普里姆算法
算法步骤:
首先将初始顶点u加入U中,对其余的每一个顶点vj,将closedge[j]均初始化未到u的边信息。
循环n - 1次,做如下处理:
从各组边closedge中选出最小边closedge[k],输出此边;
将k加入U中;
更新剩余的每组最小边信息closedge[j],对于V-U中的边,新增加了一条从k到j的边,如果新边的权值比closedge[j].lowcost小,则将closede[j].lowcost更新为新边的权值。
算法步骤
#define MaxNum 100
typedef struct {
char vexs[MaxNum]; // 存储顶点信息的一维数组
int arcs[MaxNum][MaxNum]; // 定义二维数组,表示邻接矩阵
int vexnum, arcnum; // 图的实际顶点数和边数
}AMGraph;
struct {
char adjvex; // 最小边在U中的那个顶点
int lowcost; // 最小边上的权值
}closedge[MaxNum];
int LocateVex(AMGraph &G, char v) {
for(int i = 0; i < G.vexnum; i++) {
if(v == G.vexs[i]) {
return i;
}
}
return -1;
}
// 无向网G以邻接矩阵形式存储,从顶点u出发构造G的最小生成树T,输出T的各条边
void MiniSpanTree_Prim(AMGraph G, char u) {
int k = LocateVex(G, u); // k为顶点u的下标
for(int i = 0; i < G.vexnum; i++) {
int j;
if(j != k)
closedge[j] = {u, G.arcs[k][j]}; // 对U-V的每一个顶点初始化closedge[j]
}
closedge[k].lowcost = 0;
for(int i = 1; i < G.vexnum; i++) {
k = closedge[0].lowcost;
for(int j = 1; j < G.vexnum; j++) {
if(closedge[j].lowcost < k && closedge[j].lowcost != 0) {
k = closedge[j].lowcost;
}
}
char u0 = closedge[k].adjvex;
char v0 = G.vexs[k];
closedge[k].lowcost = 0;
for(int j = 0; j < G.vexnum; j++) {
if(G.arcs[k][j] < closedge[j].lowcost) {
closedge[j] = {G.vexs[k], G.arcs[k][j]};
}
}
}
}
算法分析
普里姆算法时间复杂度为O(n^2),与网中的边数无关,因此适用于求稠密网的最小生成树。
三、克鲁斯卡尔算法
1.克鲁斯卡尔算法的构造过程
假设连通网N = (V,E),将N中的边按权值从小到大的顺序排序。
(1)假设初始状态为只有n个顶点而无边按权值从小到大的顺序排序。
(2)在E中选择权值最小的边,若该边依附的顶点落在T中不同的连通分量上(不形成回路),则将此边加入T中,否则舍去此边而选择下一条权值最小的边。
(3)重复(2),直至T中所有顶点都在同一连通分量上为止。
2.克鲁斯卡尔算法的实现
算法步骤
(1)将数组Edge中的元素按权值从小到大排序。
(2)依次查看数组Edge中的边,循环执行以下操作:
依次从排好序的数组Edge中选出一条边(v1, v2);
在Vexset中分别查找v1和v2所在的连通分量vs1和vs2进行判断:
如果vs1和vs2不等,表明所选的两个顶点分别属于不同的连通分量,输出此边,并合并vs1和vs2两个连通分量;
如果vs1和vs2相等,表明所选的两个顶点属于同一个连通分量,舍去此边而选择下一条权值最小的边。
算法描述
void MiniSpanTree_ Kruskal(AMGraph G) {
Sort(Edge);
for(int i = 0; i < G.vexnum; i++) {
Vexset[i] = i;
}
for(int i = 0; i < G.arcnum; i++) {
v1 = LocateVex(G, Edge[i].Head);
v2 = LocateVex(G, Edge[i].Tail);
vs1 = Vexset[v1];
vs2 = Vexset[v2];
if(vs1 != vs2) {
for(int j = 0; j < G.vexnum; j++) {
if(Vexset[j] == vs2)
Vexset[j] = vs1;
}
}
}
}
总结
主要都是参考文献。