一、问题介绍
生成树被定义为:一个连通无向图的生成子图,同时要求是树。也即在图的边集中选择 n − 1 n-1 n−1 条,将所有顶点连通。
我们将最小生成树(Minimum Spanning Tree,MST)定义为:无向连通图的为边权和最小的生成树。
只有连通图才有生成树,而对于非连通图,只存在生成森林。
现在广泛使用的最小生成树算法主要有两种,分别为 Kruskal 算法和 Prim 算法,下面具体介绍一下每个算法。
二、Kruskal算法
1.简介
Kruskal 算法是一种常见并且好写的最小生成树算法,由 Kruskal 发明。该算法的基本思想是从小到大加入边,是个贪心算法,一般需要借助并查集这一数据结构来实现。
2.算法流程
- 将所有的边按边权从小到大排序,这里使用了贪心的思想
- 维护一个森林,按顺序取出边,查询边的两个结点是否在同一棵树中
- 如果在,则不进行任何操作。
- 如果不在,则连接两棵树,将这条边计入最小生成树。
- 查询两点是否在同一棵树和连接两树使用并查集维护。
- 时间复杂度 O ( m log m ) O(m\log m) O(mlogm)
3.贪心证明
思路很简单,为了造出一棵最小生成树,我们从最小边权的边开始,按边权从小到大依次加入,如果某次加边产生了环,就扔掉这条边,直到加入了 n − 1 n-1 n−1 条边,即形成了一棵树。
证明:使用归纳法,证明任何时候 Kruskal 算法选择的边集都被某棵 MST 所包含。
-
基础:对于算法刚开始时,没有任何一条边,显然成立(最小生成树存在)。
-
归纳:假设某时刻成立,当前边集为 F,令 T 为这棵 MST,考虑下一条加入的边 e。
- 如果 e 属于 T,那么成立。
- 否则,T+e 一定存在一个环,考虑这个环上不属于 F 的另一条边 f(一定只有一条)。
首先,f 的权值一定不会比 e 小,不然 f 会在 e 之前被选取。
然后,f 的权值一定不会比 e 大,不然 T+e-f 就是一棵比 T 还优的生成树了。
因此,f 和 e 的权值相等,T+e-f 也是一棵最小生成树,且包含了 F。
4.模板代码
ll kruskal(ll n,ll m)
{
ll sum=0;
for(ll i=1;i<=n;i++)
fa[i]=i;
sort(e,e+m,cmp);
for(ll i=0;i<m;i++)
{
ll fu=get(e[i].u);
ll fv=get(e[i].v);
if(fu!=fv)
{
fa[fv]=fu;
sum+=e[i].w;
}
}
return sum;
}
三、Prim算法
1.简介
Prim 算法是另一种常见并且好写的最小生成树算法。该算法的基本思想是从一个结点开始,不断加点,这和以加边的方式进行的 Kruskal 算法有所不同。在算法的流程上与 Dijkstra有更多的相似之处。
2.算法流程
其实跟 Dijkstra 算法一样,每次找到距离最小的一个点,以及用新的边更新其他结点的距离,可以暴力找也可以用堆维护。
- 从任意一个结点开始,将结点分成两类:已加入的,未加入的。
- 每次从未加入的结点中,找一个与已加入的结点之间边权最小值最小的结点(dijkstra 算法这里是找的离源点最近的点)。
- 然后将这个结点加入,并连上那条边权最小的边.
- 重复 n-1 次即可。
堆优化的方式类似 Dijkstra 的堆优化,但如果使用二叉堆等不支持 O ( 1 ) O(1) O(1) decrease-key 的堆,复杂度就不优于 Kruskal,常数也比 Kruskal 大。所以,一般情况下都使用 Kruskal 算法。而在稠密图尤其是完全图上,暴力 Prim 的复杂度比 Kruskal 优,但 不一定 实际跑得更快。
暴力: O ( n 2 + m ) O(n^2+m) O(n2+m);二叉堆: O ( ( n + m ) log n ) O((n+m)\log n) O((n+m)logn);Fib 堆: O ( n log n + m ) O(n \log n + m) O(nlogn+m)
3.证明
证明:在每一步,都存在一棵最小生成树包含已选边集。
- 基础:只有一个结点的时候,显然成立。
- 归纳:如果某一步成立,当前边集为 F,属于 T 这棵 MST,接下来要加入边 e。
- 如果 e 属于 T,那么显然成立。
- 否则,考虑 T+e 中形成的环上,另一条可以加入当前边集的边 f。
首先,f 的权值一定不小于 e 的权值,否则就会选择 f 而不是 e 了。
然后,f 的权值一定不大于 e 的权值,否则 T+e-f 就是一棵更小的生成树了。
因此,f 和 e 的权值相等,T+e-f 也是一棵最小生成树,且包含了 F。
4.模板代码
void prim()
{
memset(d,0x3f,sizeof(d));
memset(vis,false,sizeof(vis));
d[1]=0;
for(ll i=1;i<n;i++)
{
ll x=0;
for(ll j=1;j<=n;j++)
if(!vis[j] && (x==0 || d[j]<d[x]))
x=j;
if(d[x]!=d[0])
cnt++;
vis[x]=true;
for(ll y=1;y<=n;y++)
if(!vis[y])
d[y]=min(d[y],mp[x][y]);
}
}