前言
本节介绍图论中一类经典问题——最小生成树
定义
生成树:
在一个无向连通图中,如果存在一个连通子图包含原图中所有的结点和部分边,且这个子图不存在回路,那么我们称这个子图为原图的一棵生成树。
最小生成树(MST):
在带权图中,所有的生成树中边权的和最小的那棵(或几棵)被称为最小生成树。
最小生成树问题是图论中最经典的问题之一,它在实际生活当中也有广泛的应用:
如在通信基站之间修建通信光缆使所有的基站间可以直接或间接通信,最少需要多少长的光缆。
求解最小生成树
要利用最小生成树来解决实际问题,我们必须先学会怎样求解一个连通图的最小生成树。
定理(Kruskal算法原理)
在要求解的连通图中,任意选择一些点属于集合A,剩余的点属于集合B,必定存在一棵最小生成树包含两个顶点分别属于集合A和集合B的边(即连通两个集合的边)中权值最小的边。
定理证明(反证法)
设连通结点集A和结点集B的边中权值最小的一条为E,在该图所有的最小生成树中都不包含该边。但在任意一棵最小生成树中必有一条边连通集合A和集合B(若没有则两集合不连通,若大于一条则出现回路),设该边为E’。
由命题假设可知,E的权值不大于E’的权值,若我们用边E替换边E’,替换后子图依然为原图的一棵生成树,该生成树的权值为原最小生成树的权值减去E’的权值后加上E的权值,该值将不会大于原最小生成树的权值,那么新的生成树也是原图的一棵最小生成树,我们就得到了一棵包含边E的最小生成树,与假设矛盾,故原命题得证。
Kruskal算法
基本思想
以边为主导地位,始终选择当前可用的最小边权的边(可以快排或STL的sort)。
每次选择边权最小的边链接两个端点是kruskal的规则,并实时判断两个点之间有没有间接联通(若True则跳过)。
步骤
- 初始时所有结点属于孤立的集合。
- 按照边权递增顺序遍历所有的边,若遍历到的边两个顶点仍分属不同的集合(该边即为连通这两个集合的边中权值最小的那条)则确定该边为最小生成树上的一条边,并将这两个顶点分属的集合合并。
- 遍历完所有边后,原图上所有结点属于同一个集合则被选取的边和原图中所有结点构成最小生成树;否则原图不连通,最小生成树不存在。
如步骤所示,在用Kruskal算法求解最小生成树的过程中涉及到大量的集合操作,我们恰好可以使用上一节中讨论的并查集来实现这些操作。
例5.3 还是畅通工程
题目描述
解题思路
在给定的道路中选取一些,使所有的城市直接或间接连通且使道路的总长度最小,该例即为典型的最小生成树问题。
我们将城市抽象成图上的结点,将道路抽象成连接点的边,其长度即为边的权值。经过这样的抽象,我们求得该图的最小生成树,其上所有的边权和即为所求。
C++代码
//
// Created by PM on 2020/2/20 21:44
//
using namespace std;
int Tree[N];
//查找代表集合的树的根结点(并查集固定函数写法)
int findRoot(int x) {
if (Tree[x] == -1) return x;
else {
int tmp = findRoot(Tree[x]);
Tree[x] = tmp;
return tmp;
}
}
//边结构体
struct Edge {
int a , b; //边两个顶点的编号
int cost; //该边的权值
//重载小于号使其可以按照边权从小到大排列
bool operator < (const Edge &A) const {
return cost < A.cost;
}
}edge[6000];
int main () {
int n;
while (scanf ("%d",&n) != EOF && n != 0) {
for (int i = 1;i <= n * (n - 1) / 2;i ++) {
scanf ("%d%d%d",&edge[i].a,&edge[i].b,&edge[i].cost);
} //输入
sort(edge + 1,edge + 1 + n * (n - 1) / 2); //按照边权值递增排列所有的边
//初始时所有的结点都属于孤立的集合
for (int i = 1;i <= n;i ++)
Tree[i] = -1;
int ans = 0; //最小生成树上边权的和,初始值为0
//按照边权值递增顺序遍历所有的边
for (int i = 1;i <= n * (n - 1) / 2;i ++) {
int a = findRoot(edge[i].a);
int b = findRoot(edge[i].b); //查找该边两个顶点的集合信息
//若它们属于不同集合,则选用该边
if (a != b) {
Tree[a] = b; //合并两个集合
ans += edge[i].cost; //累加该边权值
}
}
//最后记得要判断是否存在得不到最小生成树的情况(即所有节点是否属于同一个集合)
printf("%d\n",ans); //输出
}
return 0;
}
该例不存在得不到最小生成树的情况,所以最后我们并没有对所有结点是否属于同一个集合进行判断,若可能出现不存在最小生成树的情况,则该步不能省略。那么,怎么判断MST是否存在呢???
对Tree中每个节点进行遍历,通过findRoot()来判定是否每个点所在的根节点都相同。
写在最后
最小生成树算法除了本节所讨论的Kruskal算法,还有其他一些算法,比较有名的还有Prim算法。但两者在机试题上应用时差别不大,所以这里不再讨论。
只需牢记Kruskal算法的算法原理和相关编码技巧,就能掌握机试题中的最小生成树问题。