算法复习——图算法篇之最小生成树之Kruskal算法
以下内容主要参考中国大学MOOC《算法设计与分析》,墙裂推荐希望入门算法的童鞋学习!
1. 问题回顾
问题背景、问题定义和通用框架可见算法复习——图算法篇之最小生成树之Prim算法。
2. Kruskal算法
Prim算法和Kruskal算法最大的不同是,Prim算法始终维持边集A一棵树,规避了无环图的判断,且整个过程中只有两个割集,每次都要去寻找这两个割集的轻边,并加入图中;而Kruskal算法中,每一步中可能有多个割集,按顺序加入边,但每次加入边都需要判断是否为无环图,只要不是无环图,就可以确保加入的边一定是轻边。
2.1 算法思想
- 直接实现通用框架
- 需保证边集A仍是一个无环图
- 选边时避免成环
- 需保证边集A仍是最小生成树的子集
- 每次选择当前权重最小边
- 需保证边集A仍是一个无环图
2.2 算法思想精炼
Generic-MST(G)
A ← 0
while 没有形成最小生成树 do
寻找A的安全边(u, v) // 不成环的最小边,是一种贪心策略
A ← A ∪ (u, v)
end
return A
Kruskal算法每次寻找的都是当前所有可选边中的最小边,所以中间过程是一个森林(有若干棵子树),森林不断合并子树最终形成一棵树。
全局最小边不能避免成环问题,因此需要判断所选边的顶点是否在一棵子树,如果在一棵子树,再添加这条边,就出现了环。所以如何寻找安全边就变成了算法正确性的关键。
3. 正确性证明
安全边的定义可见算法复习——图算法篇之最小生成树之Prim算法。
若每次向边集 A A A中新增安全边,可保证边集 A A A是最小生成树的子集。因为问题就变成Kruskal算法选边策略能否保证每次都选择了安全边?
证明:
不妨设当前已选边集为 A A A(即图中的蓝线),下一条选择的边是 ( i , g ) (i, g) (i,g)。
已选边集 A A A把图分成为若干棵子树,其中 ( V ′ , E ′ ) (V', E') (V′,E′)是包含顶点 i i i的子树。
构造割 ( V ′ , V − V ′ ) (V', V-V') (V′,V−V′),割不妨害边集 A A A,换言之 A A A中的边不会横跨 ( V ′ , V − V ′ ) (V', V-V') (V′,V−V′)(即边集 A A A不包含横跨边 ( b , c ) (b, c) (b,c), ( c , d ) (c, d) (c,d), ( c , f ) (c, f) (c,f), ( i , g ) (i, g) (i,g), ( i , h ) (i, h) (i,h))。 ( i , g ) (i, g) (i,g)是横跨割 ( V ′ , V − V ′ ) (V', V-V') (V′,V−V′)的最小权重边,所以 ( i , g ) (i, g) (i,g)是关于割 ( V ′ , V − V ′ ) (V', V-V') (V′,V−V′)的轻边;又由于割 ( V ′ , V − V ′ ) (V', V-V') (V′,V−V′)不妨害边集 A A A,根据安全边辨识定理,所以轻边 ( i , g ) (i, g) (i,g)为安全边。
4. 伪代码
MST-Kruskal(G)
输入:图G
输出:最小生成树
把边按照权重升序排序
T ← {}
for (u, v) ∈ E do
if u,v 不在同一子树 then
T ← T ∪ {(u, v)}
合并u, v所在子树
end
end
return T
那么问题在于如何高效判定和维护所选边的顶点是否在一棵子树?
5. 不相交集合
5.1 概念阐释
不相交集合可以高效查找顶点所属子树,也可以高效合并顶点所在子树。
不相交集合把每棵生成子树看作一个顶点集合,每个集合表示为一棵有向树,多个不相交集合构成不相交集合森林。集合元素表示为树结点,树边由子结点指向父结点,根结点有一条指向自身的边。
5.2 相关操作
-
初始化集合:创建根结点,并设置一条指向自身的边。
伪代码:
Create-Set(x)
输入:顶点x
输出:并查集
x.parent ← x return x
时间复杂度是 O ( 1 ) O(1) O(1)
-
判定顶点是否在同一集合:回溯查找树根,检查树根是否相同
伪代码:
Find-Set(x)
输入:顶点x
输出:所属连通分量
while x.parent ≠ x do x ← x.parent end return x
时间复杂度是 O ( h ) O(h) O(h), h h h为树高
-
合并集合:合并两棵树,例如 { a , b , h , g , f } ∪ { c , i } \{a, b, h, g, f\} \cup \{c, i\} {a,b,h,g,f}∪{c,i}。
合并集合的简单实现就如上图所示,找到两树根,任意连接两棵树。但是,这样有一个潜在问题,可能造成树深度过大,降低查找效率。
因此,我们要尽可能降低树高度,提高树根查找效率。所以高效的实现方法是树高小的树连接到树高大的树上。伪代码如下:
Union-Set(x)
输入:顶点 x x x, y y y
a ← Find-Set(x) b ← Find-Set(y) if a.height ≤ b.height then if a.height = b.height then a.height ← b.height + 1 end a.parent ← b end else b.parent ← a end
时间复杂度是 O ( h ) O(h) O(h)
5.3 时间复杂度
分析以上三个操作的时间复杂度,那么不禁会有一个疑问:树的高度 h h h和顶点规模 ∣ V ∣ |V| ∣V∣有何关系?
∣ V ∣ ≥ 2 h |V| \geq 2^h ∣V∣≥2h
那我们用归纳法证明一下,以上结论为什么是对的。
- 只有一个顶点,规模 ∣ V ∣ = 1 |V|=1 ∣V∣=1,高度 h = 0 h = 0 h=0,显然 1 ≥ 2 0 1 \geq 2^0 1≥20
- 假设:任意不相交集合 m m m,高度 h m h_m hm和规模 V m V_m Vm满足 V m ≥ 2 h m V_m \geq 2^{h_m} Vm≥2hm
- 归纳:两不相交结合
a
,
b
a, b
a,b拟做合并,设合并产生的新不相交集合为
c
c
c
- 若 h a ≠ h b h_a \neq h_b ha=hb: V c = V a + V b ≥ 2 h a + 2 h b ≥ 2 m a x { h a , h b } = 2 h c V_c = V_a + V_b \geq 2^{h_a}+2^{h_b} \geq 2^{max\{h_a, h_b\}} = 2^{h_c} Vc=Va+Vb≥2ha+2hb≥2max{ha,hb}=2hc(因为树高小的树要接在树高大的树下)
- 若 h a = h b h_a = h_b ha=hb: V c = V a + V b ≥ 2 h a + 2 h b = 2 h a + 1 = 2 h c V_c = V_a + V_b \geq 2^{h_a}+2^{h_b} = 2^{h_a+1} = 2^{h_c} Vc=Va+Vb≥2ha+2hb=2ha+1=2hc
- 综上,所有不相交集合都满足 ∣ V ∣ ≥ 2 h |V| \geq 2^h ∣V∣≥2h,即 h ≤ l o g ∣ V ∣ h \leq log|V| h≤log∣V∣
所以, C r e a t e − S e t ( x ) Create-Set(x) Create−Set(x)的时间复杂度是 O ( 1 ) O(1) O(1), F i n d − S e t ( x ) Find-Set(x) Find−Set(x)的时间复杂度是 O ( h ) = O ( l o g ∣ V ∣ ) O(h)=O(log|V|) O(h)=O(log∣V∣), U n i o n − S e t ( x ) Union-Set(x) Union−Set(x)的时间复杂度是 O ( l o g ∣ V ∣ ) O(log|V|) O(log∣V∣)。
5.4 Kruskal优化伪代码
MST-Kruskal(G)
输入:图G
输出:最小生成树
把边按照权重升序排序
为每个顶点建不相交集
T ← {}
for (u, v) ∈ E do
if Find-Set(u) ≠ Find-Set(v) then
T ← T ∪ {(u, v)}
Union-Set(u, v)
end
end
return T
时间复杂度分析:
排序的时间复杂度是 O ( ∣ E ∣ l o g ∣ E ∣ ) O(|E|log|E|) O(∣E∣log∣E∣);建立不相交集的时间复杂度是 O ( ∣ V ∣ ) O(|V|) O(∣V∣);判断两个点是否在一个子树内和合并两个子树的时间复杂度是 O ( l o g ∣ V ∣ ) O(log|V|) O(log∣V∣),执行最小生成树算法的时间复杂度是 O ( ∣ E ∣ l o g ∣ V ∣ ) O(|E|log|V|) O(∣E∣log∣V∣);所以最终整体算法的时间复杂度是 O ( ∣ E ∣ l o g ∣ E ∣ + ∣ E ∣ l o g ∣ V ∣ ) O(|E|log|E|+|E|log|V|) O(∣E∣log∣E∣+∣E∣log∣V∣),假设 ∣ E ∣ = O ( ∣ V ∣ 2 ) |E|=O(|V|^2) ∣E∣=O(∣V∣2),则整体的时间复杂度是 O ( ∣ E ∣ l o g ∣ V ∣ ) O(|E|log|V|) O(∣E∣log∣V∣)。
6. 小结
Prim算法与Kruskal算法比较
通用框架 | Prim算法 | Kruskal算法 |
---|---|---|
成环判断 | 始终保持一棵树,不断扩展 | 森林合成一棵树,不相交集合 |
轻边发现 | 优先队列 | 全部边排序 |
求解视角 | 微观视角,基于当前点选边 | 宏观视角,基于全局顺序选边 |
算法思想 | 都是采用贪心策略的图算法 | 都是采用贪心策略的图算法 |