最小生成树(Minimum Spanning Tree,简称MST)是图论中的一个重要概念,主要用于解决加权无向图中连接所有顶点且总权重最小的树结构问题。本文对两种经典的算法即Prim算法和Kruskal算法进行回顾,并对后者的正确性给出简单的证明。
一. 定义
最小生成树是给定连通加权无向图 G = ( V , E ) G = (V, E) G=(V,E) 中的一棵生成树,满足以下条件:
- 包含所有顶点:生成树覆盖了图中的所有顶点。
- 边权之和最小:生成树中所有边的权重之和最小。
- 无环性:生成树中不包含任何环路。
形式化定义:假设 T T T 是 E E E 的一个子集,且 ( V , T ) (V, T) (V,T) 是一棵树,使得 w ( T ) w(T) w(T)(即 T T T 中所有边的权重之和)最小,则 T T T 为 G G G 的最小生成树。
二. 基本概念
- 割:将图 G G G 的顶点集分为不相交的两部分,假设一个子集为 U U U, 则另一子集为 V − U V-U V−U。如下图所示: U = { 0 , 4 , 5 } , V − U = { 1 , 2 , 3 , 6 } U=\{0,4,5\},V-U=\{1,2,3,6\} U={0,4,5},V−U={1,2,3,6}。
- 跨边:穿过割的边称为跨边。如下图所示:
(
0
,
1
)
,
(
4
,
6
)
,
(
4
,
3
)
(0,1),(4,6),(4,3)
(0,1),(4,6),(4,3) 为这一割的3条跨边。
三. 基本性质
性质1:若
(
u
,
v
)
(u,v)
(u,v) 是某一割的最短跨边,那么必然存在一棵MST包含
(
u
,
v
)
(u,v)
(u,v)。
推论1:若
(
u
,
v
)
(u,v)
(u,v) 不是任意割的最短跨边,那么任意MST均不包含它。
推论2:任意一棵MST也必然包含任意割的最短跨边
(
u
,
v
)
(u,v)
(u,v)。
反证法证明性质1:
如下图所示,假设
(
u
,
v
)
(u,v)
(u,v) 未被任何MST采用,那么任取一棵MST,连接
(
u
,
v
)
(u,v)
(u,v) 必然会构成唯一的回路,且该回路必然包含
(
u
,
v
)
(u,v)
(u,v) 以及另一条跨边
(
s
,
t
)
(s,t)
(s,t)。 此时,若断开
(
s
,
t
)
(s,t)
(s,t), 那么我们将得到一棵新的生成树,且权重之和更小,于是我们得到了更小的MST。 这与假设不符,因此,若
(
u
,
v
)
(u,v)
(u,v) 是某一割的最短跨边,那么必然存在一棵MST包含
(
u
,
v
)
(u,v)
(u,v)
四. Prim算法
4.1 Prim算法描述
从
T
1
=
(
{
v
1
}
;
∅
)
T_1 = (\{v_1\}; \varnothing)
T1=({v1};∅) 开始,逐步构造
T
2
、
T
3
、
.
.
.
、
T
n
T_2、T_3、...、T_n
T2、T3、...、Tn。其中,
v
1
v_1
v1 可以任选。
T
k
=
(
V
k
;
E
k
)
,
∣
V
k
∣
=
k
,
∣
E
k
∣
=
k
−
1
,
V
k
⊂
V
k
+
1
T_k = (V_k; E_k),|V_k| = k,|E_k| = k-1,V_k \subset V_{k+1}
Tk=(Vk;Ek),∣Vk∣=k,∣Ek∣=k−1,Vk⊂Vk+1。
由以上分析,为由
T
k
T_k
Tk 构造
T
k
+
1
T_{k+1}
Tk+1 ,
只需将
(
V
k
:
V
−
V
k
)
(V_k : V-V_k)
(Vk:V−Vk) 视作原图的一个割, 并在该割的所有跨边中,
找出极短者
e
k
=
(
v
k
,
u
k
)
e_k = (v_k, u_k )
ek=(vk,uk) ,
则
T
k
+
1
=
(
V
k
+
1
;
E
k
+
1
)
=
(
V
k
∪
u
k
;
E
k
∪
e
k
)
T_{k+1} = (V_{k+1}; E_{k+1}) = (V_k \cup {u_k}; E_k \cup {e_k} )
Tk+1=(Vk+1;Ek+1)=(Vk∪uk;Ek∪ek) 。
4.2 Prim算法示例
4.3 Prim算法代码
#define INF 32767 //INF表示∞, 邻接矩阵中不可达边的权重为∞
int dis[1000], inMST[1000], mst = 0; //dis: V-U 到 U的最短距离 mst: 最小生成树的权重和
void Prim(vector<vector<int>>& G, int v){
inMST[v] = 1;
for (int i = 0; i < G.size(); i++)
dis[i] = G[v][i];
for (int i = 1; i < G.size(); i++) { //循环n-1次
int min = INF, k;
for (int j = 0; j < G.size(); j++) //在(V-U)中找出离U最近的顶点k
if (inMST[j] == 0 && dis[j] < min)
min = dis[j], k = j; // min: 最短距离,k:最近顶点编号
mst += min;
inMST[k] = 1; //标记k已经被加入
for (int j = 0; j < G.size(); j++) //更新其它点到U的距离
if (inMST[j] == 0 && G[k][j] < dis[j])
dis[j] = G[k][j];
}
}
五. Kruskal算法
5.1 算法描述
- 将图 G = ( V , E ) G = (V, E) G=(V,E) 中的所有边按权重非降序排序。设排序后的边序列为 e 1 , e 2 , . . . , e ∣ E ∣ e_1, e_2, ..., e_{|E|} e1,e2,...,e∣E∣。
- 初始化一个空的边集 T = { } T = \{\} T={},用于存放 MST 的边。
- 按排序后的顺序 (
i
=
1
i = 1
i=1 到
∣
E
∣
|E|
∣E∣),依次考虑每条边
e
i
e_i
ei:
- 如果将 e i e_i ei 加入 T T T 后, T T T 中不会形成环,则将 e i e_i ei 加入 T T T ( T = T ∪ { e i } T = T ∪ \{e_i\} T=T∪{ei})。
- 否则(即加入 e i e_i ei 会形成环),丢弃 e i e_i ei。
- 当 T T T 中包含 ∣ V ∣ − 1 |V| - 1 ∣V∣−1 条边时,算法终止。 T T T 即为所求的 MST。
5.2 Kruskal算法示例
5.3 Kruskal算法代码
int parent[maxn], mst_w;
struct Edge { int from, to, w; } edges[maxn];
bool compare(Edge e1, Edge e2) { return e1.w <= e2.w; }
int Find(int x) { //查找结点x的根(集合代表)
if (x == parent[x]) //找到根
return x; //返回根(集合代表)
else
return parent[x] = Find(parent[x]); //递归向上找根
}
void Union(int x,int y) { //合并x和y所在的集合
int px = Find(x);
int py = Find(y);
if (px != py) //如果两个集合不同则合并
parent[px] = py;
}
int Kruskal(int en) {
for (int i = 0; i < maxn; i++) parent[i] = i; //初始化并查集
sort(edges, edges + en, compare);
for (int i = 0; i < en; i++) {
int p1 = Find(edges[i].from);
int p2 = Find(edges[i].to);
if (p1 != p2) {
Union(p1, p2);
mst_w += edges[i].w;
}
}
return mst_w;
}
5.4 Kruskal算法正确性证明
接下来,简单证明一下这个算法的正确性。本文的证明方式跟绝大多数书上和网上的证明方式不同,证明主要基于上面提到的一个性质和两个推论:
性质1:若
(
u
,
v
)
(u,v)
(u,v) 是某一割的最短跨边,那么必然存在一棵MST包含
(
u
,
v
)
(u,v)
(u,v)。
推论1:若
(
u
,
v
)
(u,v)
(u,v) 不是任意割的最短跨边,那么任意MST均不包含它。(性质1的逆否命题)
推论2:任意一棵MST也必然包含任意割的最短跨边
(
u
,
v
)
(u,v)
(u,v)。
为了证明Kruskal算法的正确性,需要证明两点:
- 因为构成了回路而放弃的边不属于任意MST。
- 按照边长顺序加入的边必然都属于某棵MST。
1)首先证明:因为构成了回路而放弃的边不属于任意MST。
首先,如果一个图中的所有边的权重都不相同时,这个图对应的MST是唯一的。这一点不难证明,因为在Prim算法中,每一步都只能选择唯一的一条最短跨边。但图中确实可能存在权重相同的边,处理方式也很简单,就是对边的权重做稍微扰动(针对某条边,扰动后要保证,权重原本比它小的边扰动后依然比它小,权重原本比它大的边扰动后依然比它大,即不改变相对顺序),使之各不相同,那么MST也是唯一的。
这里我们就假设给定图
G
=
(
V
,
E
)
G = (V, E)
G=(V,E) 的所有边的权重均不相同。
在Kruskal算法中,因为构成了回路而放弃的一些边。 我们说这些边不属于MST。
如下图所示,假设在Kruskal算法的某一步,我们将边
e
k
=
(
v
i
,
v
j
)
e_k=(v_i,v_j)
ek=(vi,vj) 加入之后,在局部构成了回路。
在这个回路中(上图红色的边),
e
k
=
(
v
i
,
v
j
)
e_k=(v_i,v_j)
ek=(vi,vj) 是最后加入的边,因此它是这个回路中权重最大的边。
进一步,如果将
e
k
=
(
v
i
,
v
j
)
e_k=(v_i,v_j)
ek=(vi,vj) 作为一个跨边 (即将
v
i
v_i
vi 和
v
j
v_j
vj 放到两个不相交的子集中),我们发现这个割无论如何都会穿过这个回路中的其它边。
又因为其他边的权重都大于
e
k
e_k
ek, 因此
e
k
e_k
ek 是不是任意割的最短跨边。
所以
e
k
e_k
ek 不包含在MST中。
2)然后证明:按照边长顺序加入的边必然都属于某棵MST。
不失一般性,如下图所示,我们仍然假设接下来考查的是
e
k
=
(
v
i
,
v
j
)
e_k=(v_i,v_j)
ek=(vi,vj)。它是否在MST中呢? 只要它在某一割的最短跨边,那么它就在MST中。
我们要将
e
k
=
(
v
i
,
v
j
)
e_k=(v_i,v_j)
ek=(vi,vj) 构造为一个最短跨边,就要构造一个割,且这一割不经过别的更短的跨边(也就是不穿过那些已经在MST中的跨边)。为此,我么可以将那些已经在MST子树中的点缩点即可,这样割就不会穿过比
e
k
=
(
v
i
,
v
j
)
e_k=(v_i,v_j)
ek=(vi,vj) 更短的跨边了。
如下图所示,缩点后,图中仅剩3个点,我们可以很容易的构造一个割将
v
i
v_i
vi 和
v
j
v_j
vj 放到两个不相交的子集中。且
e
k
=
(
v
i
,
v
j
)
e_k=(v_i,v_j)
ek=(vi,vj) 是该割的最短跨边,尽管可能还有其它跨边,但是Kruskal算法是按顺序遍历的边,所有还未遍历到的边的权重必然大于
e
k
=
(
v
i
,
v
j
)
e_k=(v_i,v_j)
ek=(vi,vj)。
为此,Kruskal算法的正确性得证。
六. Prim算法 和 Kruskal算法的局限性
我们一般讲最小生成树,只针对无向图,对于有向图是有问题的,如下图所示。
有向图的最小生成树->最小树形图: https://oi-wiki.org/graph/dmst/