文章目录
要点8:掌握最小生成树算法(Kruskal 算法和 Prim 算法)
📌 适合对象:算法学习者、计算机科学学生
⏱️ 预计阅读时间:70-80分钟
🎯 学习目标:掌握两种最小生成树算法(Kruskal和Prim),理解它们的原理、实现和复杂度
📚 参考PPT:第 11 章-PPT-N2 v3.1(最小生成树算法)- Kruskal算法、Prim算法相关内容
📚 学习路线图
本文内容一览(快速理解)
- 最小生成树(MST):加权图的一棵生成树,其所有边的总权值最小
- 通用贪心策略:每次选择一条安全边加入集合A
- Kruskal算法:按边权从小到大,合并不同的连通分支
- Prim算法:从单个顶点开始,每次添加距离树最近的顶点
- 算法比较:Kruskal适合稀疏图,Prim适合稠密图
一、最小生成树问题(Minimum Spanning Tree Problem):问题定义
这一章要建立的基础:理解最小生成树是图算法中的经典问题,有多种应用场景。
核心问题:给定一个带权连通图,如何找出一棵生成树,使得所有边的总权值最小?
[!NOTE]
📝 关键点总结:连通图 G G G的一个连通子图如果包含图中所有顶点则称为 G G G的一个支撑子图(spanning subgraph)。图 G G G的一个支撑子图如果是一棵树,则称其为图 G G G的一棵生成树(Spanning Tree)。加权图 G G G的一棵生成树 T T T,如果它的所有边的总权值,记作 W ( T ) W(T) W(T),是所有生成树中最小的,则称其为最小生成树(MST,Minimal Spanning Tree)。
1.1 问题定义(Problem Definition):理解MST
概念的本质:
最小生成树问题的定义:
- 输入:带权连通无向图 G ( V , E , w ) G(V, E, w) G(V,E,w)
- 输出:一棵生成树 T T T,使得 W ( T ) = ∑ e ∈ T w ( e ) W(T) = \sum_{e \in T} w(e) W(T)=∑e∈Tw(e)最小
树的特性:
- 树是一个连通的、无环的无向图
- 一个可能不连通的无向无环图称为森林
- 树中任意两顶点由唯一简单路径连接
- 树是连通的,且 ∣ E ∣ = ∣ V ∣ − 1 |E| = |V| - 1 ∣E∣=∣V∣−1
图解说明:
💡 说明:很多应用问题可建模为找MST的问题,比如电子电路设计中的多管角连接线路总长度最小化、通信网络中的最小代价全网广播等。有时,最小生成树未必唯一,有些网络中可能存在多棵权重相同的最小生成树。
类比理解:
就像连接城市:
- 顶点:城市
- 边:连接城市的道路
- 权重:道路的长度或成本
- MST:用最少的成本连接所有城市
实际例子:
最小生成树示例:
图:a --5-- b
| |
3 2
| |
c --1-- d
可能的生成树:
1. a-b-d-c:5+2+1 = 8
2. a-c-d-b:3+1+2 = 6 ✓(MST)
3. a-b-d:5+2 = 7(不连通,不是生成树)
MST:a-c-d-b,总权值6
二、通用贪心策略(Generic Greedy Strategy):安全边
这一章要建立的基础:理解Kruskal和Prim算法都遵循同一个通用贪心策略。
核心问题:如何用贪心法构造最小生成树?
[!NOTE]
📝 关键点总结:Kruskal和Prim算法都遵循这一策略。假设带权连通图 G ( V , E ) G(V, E) G(V,E)中,顶点的数目 ∣ V ∣ = n |V| = n ∣V∣=n,边的数目 ∣ E ∣ = m |E| = m ∣E∣=m。在每次循环之前, A A A是某棵最小生成树的一个(边)子集;而每次循环之后 A ← A ∪ ( u , v ) A \leftarrow A \cup (u,v) A←A∪(u,v)仍是某棵MST的(边)子集。
2.1 通用算法框架(Generic Algorithm Framework)
概念的本质:
通用算法的步骤:
- 初始化:边的集合 A ← ∅ A \leftarrow \emptyset A←∅(含 n n n个孤立顶点,即 n n n棵树)
- 找安全边:在 E E E中找出一条边 e e e使得集合 A ∪ { e } A \cup \{e\} A∪{e}属于某棵MST中
- 添加边: A ← A ∪ { e } A \leftarrow A \cup \{e\} A←A∪{e}
- 判断终止:如果当前的边集合 A A A构成一棵生成树,算法停止,否则goto第二步
算法伪码:
Generic-MST (G, w)
A ← ∅
while A does not form a spanning tree //只要A还不是一棵生成树
find a safe edge (u, v) for A //找一条安全的边(u, v)
A ← A ∪ {(u, v)} //把边(u, v)加到集合A中
endwhile
return A
End
图解说明:
💡 说明:随着生成树构造过程的推进,集合 A A A总是保持在无环状态,否则包含集合 A A A的MST将包含一个环路,与树的定义相矛盾。算法执行的任何时刻, T ( V , A ) T(V, A) T(V,A)是一个森林, T T T的每一个连通分支是一棵树。Generic-MST算法中的while循环总执行次数为 ∣ V ∣ − 1 |V|-1 ∣V∣−1。
实际例子:
通用算法执行过程:
初始:A = ∅,有4个孤立顶点
第1轮:选择安全边(a,b),A = {(a,b)}
第2轮:选择安全边(c,d),A = {(a,b), (c,d)}
第3轮:选择安全边(b,c),A = {(a,b), (c,d), (b,c)}
结果:A构成生成树,算法结束
2.2 安全边(Safe Edge):割的最小交叉边
概念的本质:
割(Cut):图的一个顶点分割 C = ( P , V − P ) C = (P, V-P) C=(P,V−P),就是把图的顶点集合 V V V分成两个非空子集 P P P和 V − P V-P V−P。
交叉边(Cross Edge):给定一个割 C = ( P , V − P ) C = (P, V-P) C=(P,V−P),如果一条边 ( u , v ) (u, v) (u,v)的两端, u u u和 v v v分属于两边,即 u ∈ P u \in P u∈P和 v ∈ V − P v \in V-P v∈V−P,那么我们说,割 C C C与边 ( u , v ) (u, v) (u,v)相交,而边 ( u , v ) (u, v) (u,v)是一条交叉边。
割尊重集合A:如果图中一个割 C = ( P , V − P ) C = (P, V-P) C=(P,V−P),与一个边的集合 A ⊆ E A \subseteq E A⊆E中每一条边都不相交,那么,我们说这个割尊重(respect)集合 A A A。
最小交叉边:给定一个割 C = ( P , V − P ) C = (P, V-P) C=(P,V−P),其所有交叉边构成的集合,称为这个割的交集,记为 B ( C ) B(C) B(C)。交集 B ( C ) B(C) B(C)中权值最小的边称为最小交叉边。
定理9.1:对于一个带权连通图 G ( V , E , w ) G(V, E, w) G(V,E,w), A ⊆ E A \subseteq E A⊆E是 E E E的一个子集且包含在某棵MST之中。设 C = ( P , V − P ) C = (P, V-P) C=(P,V−P)是图 G G G中任意一个与 A A A不相交的割,则这个割的最小交叉边对于 A A A来说是一条安全边。
图解说明:
💡 说明:定理9.1的证明思路:假设 T T T是一棵包含集合 A A A的MST,而边 ( u , v ) (u, v) (u,v)是割 C C C的最小交叉边。如果 ( u , v ) ∉ T (u, v) \notin T (u,v)∈/T,则 T T T中必有一条连接 u u u和 v v v的路径 p p p,路径 p p p上必有另外一条交叉边 ( x , y ) (x, y) (x,y)。把边 ( x , y ) (x, y) (x,y)从 T T T中刪去,把边 ( u , v ) (u, v) (u,v)加进去,得到新的生成树 T ′ T' T′。因为 ( u , v ) (u, v) (u,v)是最小交叉边, w ( u , v ) ≤ w ( x , y ) w(u, v) \leq w(x, y) w(u,v)≤w(x,y),所以 W ( T ′ ) ≤ W ( T ) W(T') \leq W(T) W(T′)≤W(T), T ′ T' T′也是一棵MST。
实际例子:
安全边示例:
图:a --5-- b
| |
3 2
| |
c --1-- d
当前A = {(a,c)}(权值3)
割C = ({a,c}, {b,d})
交叉边:{(a,b), (c,d), (c,b)}
最小交叉边:(c,d),权值1
根据定理9.1,(c,d)是安全边,可以加入A
三、Kruskal算法:合并森林
这一章要建立的基础:理解Kruskal算法通过合并不同的连通分支来构造MST。
核心问题:如何通过合并森林来构造最小生成树?
[!NOTE]
📝 关键点总结:在Kruskal算法中,集合 A A A是一个森林,每次加入到集合 A A A中的安全边永远都是连接两个不同分支(即:子树)的所有边中,权重最小的一个。Kruskal算法要的是怎么合并 ∣ V ∣ |V| ∣V∣棵树。
3.1 Kruskal算法的基本思想(Basic Idea):按边权排序
概念的本质:
Kruskal算法的策略:
- 初始化:集合 A A A初始化为空集,图 T T T含有 n n n个孤立顶点
- 排序:把图 G G G中的边按权值排序使得 e 1 ≤ e 2 ≤ … ≤ e m e_1 \leq e_2 \leq \ldots \leq e_m e1≤e2≤…≤em
- 选择:按序【从小到大】逐条边检查并做选择
- 判断:如果 A ∪ { e i } A \cup \{e_i\} A∪{ei}不产生回路,那么就把 e i e_i ei选上并加到 A A A中
- 终止:当 A A A包含 n − 1 n-1 n−1条边时,算法结束
算法伪码:
MST-Kruskal(G(V, E)) //G是一个带权连通图
1. A ← ∅ //集合A初始化为空集
2. Construct graph T(V, A) //初始时,图T含有n个孤立顶点
3. 把图G中的边按权值排序使得e1 ≤ e2 ≤ … ≤ em
4. for i ← 1 to m //按序【从小到大】逐条边检查并做选择
5. if A∪{ei} does not form a cycle //把边ei加到A中不产生回路
6. then A ← A ∪ {ei} //那么就把ei 选上并加到A中
7. endif
8. endfor
9. return graph T(V, A) //由V和A中边组成的图就是解
10. End
图解说明:
💡 说明:总结来说,Kruskal算法每次都把图中距离最近的两个分支联起来,一直持续这一进程,直到网络中只剩一下一个分支(即一棵最小生成树)。每次所选的连通相距最近两个分支的边,就可以理解成当时所有穿越边中最短的那条。贪心法的精要所在。
实际例子:
Kruskal算法执行过程:
图:a --5-- b
| |
3 2
| |
c --1-- d
边排序:(c,d,1), (b,d,2), (a,c,3), (a,b,5)
第1轮:检查(c,d,1)
- 不形成回路,加入A
- A = {(c,d)}
第2轮:检查(b,d,2)
- 不形成回路,加入A
- A = {(c,d), (b,d)}
第3轮:检查(a,c,3)
- 不形成回路,加入A
- A = {(c,d), (b,d), (a,c)}
第4轮:检查(a,b,5)
- 会形成回路(a-c-d-b-a),跳过
结果:MST = {(c,d), (b,d), (a,c)},总权值6
3.2 Kruskal算法的实现(Implementation):Union-Find数据结构
概念的本质:
如何检测 A ∪ { e i } A \cup \{e_i\} A∪{ei}是否有回路?
Union-Find算法:
- A A A中每个连通分支中的顶点组织为一个集合,并分配一个分支号
- 初始时刻,每个顶点 u ∈ V ( G ) u \in V(G) u∈V(G)各自形成一个集合,用 M a k e − s e t ( u ) Make-set(u) Make−set(u)表示
- 每检查一条边
(
u
,
v
)
(u, v)
(u,v)时:
- 找出 u u u和 v v v的分支号: F i n d ( u ) Find(u) Find(u)和 F i n d ( v ) Find(v) Find(v)
- 如果 u u u和 v v v的分支号相同,边 ( u , v ) (u, v) (u,v)的加入会形成回路,不选这条边
- 否则,把这条边加到子图 A A A中,用 U n i o n ( u , v ) Union(u, v) Union(u,v)合并两个集合
改进的算法伪码:
MST-Kruskal(G(V, E))
A ← ∅
Construct graph T(V, A) //图T有顶点集合V,边的集合A
Sort edges such that e1 ≤ e2 ≤ … ≤ em //按从小到大排序..
for each vertex v ∈ V
Make-Set(v) //初始化T中每个分支
endfor
for i ← 1 to m
Let ei = (u, v)
if Find(u) ≠ Find(v)
then A ← A ∪ {ei} // ei 是一条安全边
Union(u, v) //把u和v所在子树合并
endif
endfor
return graph T(V, A)
End
图解说明:
💡 说明:Union-Find对 m m m条边的操作复杂度是 O ( m α ( n ) ) O(m \alpha(n)) O(mα(n))。 α ( n ) \alpha(n) α(n)是Ackermann函数的反函数,增长极慢,可认为常数。Kruskal算法复杂度是 O ( m log n + m α ( n ) ) = O ( m log n ) O(m \log n + m \alpha(n)) = O(m \log n) O(mlogn+mα(n))=O(mlogn)。
实际例子:
Union-Find示例:
初始:每个顶点一个集合
{a}, {b}, {c}, {d}
检查边(c,d):
Find(c) = c, Find(d) = d(不同)
加入边,Union(c,d)
集合:{a}, {b}, {c,d}
检查边(b,d):
Find(b) = b, Find(d) = c(不同)
加入边,Union(b,c,d)
集合:{a}, {b,c,d}
检查边(a,c):
Find(a) = a, Find(c) = b(不同)
加入边,Union(a,b,c,d)
集合:{a,b,c,d}
检查边(a,b):
Find(a) = a, Find(b) = a(相同)
形成回路,跳过
3.3 Kruskal算法的正确性证明(Correctness Proof)
概念的本质:
Kruskal算法的正确性证明:
归纳法证明:算法选中的任何一条边 e i e_i ei都使 A ∪ { e i } A \cup \{e_i\} A∪{ei}包含在某棵MST中。
归纳基础:
- 在for循环开始前, T ( V , A ) T(V, A) T(V,A)只含 ∣ V ∣ |V| ∣V∣个孤立的顶点,不含边
- 显然 A A A是包含在任意一棵MST之中
归纳步骤:
- 假设算法对前 i − 1 i-1 i−1条边做了正确选择,这时的集合 A A A包含在某棵MST中
- 我们证明算法对边 e i e_i ei的决定也是正确的
两种情况:
-
把 e i e_i ei加到子图 A A A中后产生回路:
- 这是不可能发生的,因为第5行代码已经排除了这种可能
-
把 e i e_i ei加到子图 A A A中以后没有产生回路:
- 假设 e i = ( u , v ) e_i = (u, v) ei=(u,v)
- 因为无回路,所以在加进 e i e_i ei之前, u u u和 v v v在 A A A中属于不同的连通分支
- 构造割 C = ( P , V − P ) C = (P, V-P) C=(P,V−P),其中 P P P含有 A A A中所有与顶点 u u u连通的顶点
- e i e_i ei是一条交叉边,而且是最小交叉边(因为所有权值比 e i e_i ei小的边都已检查过)
- 根据定理9.1, e i = ( u , v ) e_i = (u, v) ei=(u,v)是一个安全边
图解说明:
💡 说明:所以,Kruskal算法结束时, T ( V , A ) T(V, A) T(V,A)是一棵MST。这时, T ( V , A ) T(V, A) T(V,A)必定是一个连通图,否则由于原图 G G G是连通的,运算中一定丢弃了某条连结两个不同分支的边,这与算法矛盾。
四、Prim算法:生长树
这一章要建立的基础:理解Prim算法通过从单个顶点开始逐步生长树来构造MST。
核心问题:如何通过生长树来构造最小生成树?
[!NOTE]
📝 关键点总结:在Prim算法中,集合 A A A则一直就是一棵树,每次加入到 A A A的安全边永远是连接 A A A与 A A A之外节点的所有边中,权重最小的一个。Prim算法要的是怎么生长一棵树。
4.1 Prim算法的基本思想(Basic Idea):从单个顶点开始
概念的本质:
Prim算法的策略:
- 初始化:随机或指定图中某个顶点 s ∈ V s \in V s∈V作为树根,并设置边的集合 A A A为空集
- 与Kruskal不同:子图 A A A永远只是一棵树
- 找安全边:每次找的安全边必须与当前的子图 A A A中的某个顶点相联
- 割的定义:每次使用的割是把边集 A A A所关联的顶点(仍记为 A A A)放在割的一边,而其余顶点则放在割的另一边,即 C = ( A , V − A ) C = (A, V-A) C=(A,V−A)
- 最小交叉边:最小交叉边就是联结 A A A以外的顶点和 A A A里面的顶点的边中权值最小的那条
找最小交叉边算法:
- 为每个 A A A以外的顶点 v v v,找出 v v v到 A A A的最小交叉边 ( u , v ) (u, v) (u,v)
- 记为 d ( v ) = w ( u , v ) d(v) = w(u, v) d(v)=w(u,v), π ( v ) = u \pi(v) = u π(v)=u( u u u是 A A A中的顶点,称为 v v v的父亲)
- 初始置 d ( v ) = ∞ d(v) = \infty d(v)=∞, π ( v ) = n i l \pi(v) = nil π(v)=nil, ∀ v ∈ V \forall v \in V ∀v∈V,但 d ( s ) = 0 d(s) = 0 d(s)=0
- 一条安全边就是所有 A A A以外的顶点中对应的一条最小交叉边
图解说明:
💡 说明:Prim算法是每次把距离已经构造的树最近的【树外】顶点加到树上来,一直持续这一进程,直到图中所有顶点都加到树上来,就可以得到一棵最小生成树。每次所选的连接最近树外顶点所用的边,就是树上顶点和树外顶点所形成的割的一条最小交叉边。
实际例子:
Prim算法执行过程(从a开始):
图:a --5-- b
| |
3 2
| |
c --1-- d
初始化:A = ∅,选择a
d[a] = 0, d[b] = ∞, d[c] = ∞, d[d] = ∞
第1轮:选择a(d[a]最小)
- 更新邻居:d[b] = 5, d[c] = 3
- 加入边:无(a是根)
- A = ∅(树只有a)
第2轮:选择c(d[c] = 3最小)
- 加入边(a,c)
- 更新邻居:d[d] = min(∞, 1) = 1
- A = {(a,c)}
第3轮:选择d(d[d] = 1最小)
- 加入边(c,d)
- 更新邻居:d[b] = min(5, 2) = 2
- A = {(a,c), (c,d)}
第4轮:选择b(d[b] = 2最小)
- 加入边(d,b)
- A = {(a,c), (c,d), (d,b)}
结果:MST = {(a,c), (c,d), (d,b)},总权值6
4.2 Prim算法的实现(Implementation):伪码
概念的本质:
Prim算法的伪码:
MST-Prim(G(V, E), w, s)
1 for each v ∈ V //初始化
2 d[v] ← ∞;
3 π[v] ← nil
4 endfor
5 d[s] ← 0;
6 A ← ∅ //边的集合初始为空
7 Construct graph T(V, A) //构造一个只有顶点V但没有边的图
8 Q ← V //用数据结构Q把V中点组织起来
9 while Q ≠ ∅
10 u ← Extract-Min(Q) //找到Q中d(·)值最小的,并将其从Q中剥离
11 for each v ∈ Adj[u]
12 if v ∈ Q and d[v] > w(u, v) //检查新交叉边 并更新d[v]
13 then d[v] ← w(u, v)
14 π[v] ← u
15 endif
16 endfor
17 endwhile
18 A ← {(π[u], u): u ∈ V – {s}} //为树添加边
19 return T(V, A)
End
关键点:
- 每次循环更新得到的 d [ v ] d[v] d[v]和 π [ v ] \pi[v] π[v]只是临时值,还有可能在后续循环中更新(降低)
- 只有当一个顶点从第10行 E x t r a c t − M i n ( ) Extract-Min() Extract−Min()操作从 Q Q Q中取出的时候,那时的 d [ ] d[] d[]和 π [ ] \pi[] π[]值才永久化
图解说明:
💡 说明:Prim算法与Dijkstra算法非常相似,主要区别在于:Prim算法中 d [ v ] d[v] d[v]表示顶点 v v v到当前树 A A A的距离(一跳距离),而Dijkstra算法中 d [ v ] d[v] d[v]表示从源点 s s s到顶点 v v v的距离(累计距离)。
实际例子:
Prim算法详细执行:
图:s --1--> a
| |
4 3
| |
v v
b --2--> c
初始化:d[s]=0, d[a]=∞, d[b]=∞, d[c]=∞
Q = {s, a, b, c}
第1轮:u = s
- 更新:d[a] = 1, d[b] = 4
- Q = {a, b, c}
第2轮:u = a(d[a]=1最小)
- 加入边(s,a)
- 更新:d[c] = 3
- Q = {b, c}
第3轮:u = c(d[c]=3最小)
- 加入边(a,c)
- 更新:d[b] = min(4, 2) = 2
- Q = {b}
第4轮:u = b(d[b]=2最小)
- 加入边(c,b)
- Q = ∅
结果:MST = {(s,a), (a,c), (c,b)},总权值6
4.3 Prim算法的复杂度(Complexity):取决于数据结构
概念的本质:
Prim算法的复杂度取决于用什么数据结构来存储 d [ u ] d[u] d[u]:
-
采用数组:
- 找出最小 d [ u ] d[u] d[u]值需要 O ( n ) O(n) O(n)时间
- 完成第一件事所要的总时间为 n × O ( n ) = O ( n 2 ) n \times O(n) = O(n^2) n×O(n)=O(n2)
- 检查和更新顶点 u u u的每一个邻居只需 O ( 1 ) O(1) O(1)时间
- 第二件事所需总时间与图中边的个数成正比,为 O ( m ) O(m) O(m)
- 总复杂度: O ( n 2 ) O(n^2) O(n2)
-
采用基于堆的优先队列:
- 最小 d [ u ] d[u] d[u]可以立即在根结点那里得到,删除并修复堆需要 O ( log n ) O(\log n) O(logn)时间
- 完成第一件事所要的总时间为 n × O ( log n ) = O ( n log n ) n \times O(\log n) = O(n \log n) n×O(logn)=O(nlogn)
- 更新顶点 u u u的一个邻居 v v v需要 O ( log n ) O(\log n) O(logn)时间
- 第二件事所需总时间为 O ( m log n ) O(m \log n) O(mlogn)
- 总复杂度: O ( m log n ) O(m \log n) O(mlogn)
-
采用斐波那契堆(Fibonacci Heap):
- 在更新点 u u u的一个邻居时,我们并不立刻对堆进行修复,只是在这个点上打上记号,需要 O ( 1 ) O(1) O(1)
- 等到下一个循环做"第一件事"的时候,进行大的修复工作
- 用平摊分析的方法可以证明这第二件事所需总时间为 O ( m ) O(m) O(m)
- 总复杂度: O ( n log n + m ) O(n \log n + m) O(nlogn+m)
图解说明:
💡 说明:对于稠密图( m m m接近 n 2 n^2 n2),使用数组的Prim算法复杂度为 O ( n 2 ) O(n^2) O(n2),与使用堆的 O ( n 2 log n ) O(n^2 \log n) O(n2logn)相比更优。对于稀疏图( m m m接近 n n n),使用堆的Prim算法复杂度为 O ( n log n ) O(n \log n) O(nlogn),比数组的 O ( n 2 ) O(n^2) O(n2)更优。
实际例子:
复杂度比较:
图:n = 1000个顶点
情况1:稠密图(m = 500,000)
- 数组:O(n²) = O(1,000,000)
- 堆:O(m log n) = O(500,000 × 10) = O(5,000,000)
- 数组更优
情况2:稀疏图(m = 2000)
- 数组:O(n²) = O(1,000,000)
- 堆:O(m log n) = O(2000 × 10) = O(20,000)
- 堆更优
五、两种算法的比较(Comparison):理解它们的区别
这一章要建立的基础:理解Kruskal和Prim两种算法的区别,知道在什么情况下使用哪种算法。
核心问题:两种算法有什么相同点和不同点?如何选择合适的方法?
[!NOTE]
📝 关键点总结:Kruskal算法和Prim算法是构造最小生成树的两个经典算法,它们都基于上面的通用算法框架。在Kruskal算法中,集合 A A A是一个森林,每次加入到集合 A A A中的安全边永远都是连接两个不同分支的所有边中,权重最小的一个。在Prim算法中,集合 A A A则一直就是一棵树,每次加入到 A A A的安全边永远是连接 A A A与 A A A之外节点的所有边中,权重最小的一个。
5.1 相同点(Similarities):都使用贪心策略
概念的本质:
两种算法的相同点:
- 都使用贪心策略:每次选择一条安全边加入集合 A A A
- 都基于通用算法框架:都遵循Generic-MST的框架
- 都利用定理9.1:都通过找割的最小交叉边来找到安全边
- 都保证正确性:都能正确构造最小生成树
图解说明:
5.2 不同点(Differences):策略、数据结构、复杂度
概念的本质:
两种算法的不同点:
| 特性 | Kruskal算法 | Prim算法 |
|---|---|---|
| 集合A的性质 | 森林(多个连通分支) | 树(单个连通分支) |
| 选择策略 | 连接两个不同分支的最小边 | 连接树与树外顶点的最小边 |
| 数据结构 | Union-Find | 优先队列(堆) |
| 时间复杂度 | O ( m log n ) O(m \log n) O(mlogn) | O ( n 2 ) O(n^2) O(n2)或 O ( m log n ) O(m \log n) O(mlogn)或 O ( n log n + m ) O(n \log n + m) O(nlogn+m) |
| 适用场景 | 稀疏图 | 稠密图(数组)或稀疏图(堆) |
| 实现难度 | 需要Union-Find | 需要优先队列 |
图解说明:
💡 说明:Kruskal算法每次都把图中距离最近的两个分支联起来,一直持续这一进程,直到网络中只剩一下一个分支。Prim算法是每次把距离已经构造的树最近的【树外】顶点加到树上来,一直持续这一进程,直到图中所有顶点都加到树上来。
实际例子:
算法选择示例:
场景1:稀疏图(m接近n)
- 推荐:Kruskal算法
- 理由:O(m log n) = O(n log n),实现简单
场景2:稠密图(m接近n²)
- 推荐:Prim算法(数组实现)
- 理由:O(n²) vs O(n² log n),Prim更快
场景3:需要在线算法(边动态添加)
- 推荐:Prim算法
- 理由:可以逐步生长树
场景4:边已经排序
- 推荐:Kruskal算法
- 理由:不需要排序,直接O(m α(n))
📝 本章总结
核心要点回顾:
-
最小生成树(MST):
- 加权图的一棵生成树,其所有边的总权值最小
- 应用:电子电路设计、通信网络、近似算法
-
通用贪心策略:
- 每次选择一条安全边加入集合 A A A
- 安全边:割的最小交叉边(定理9.1)
-
Kruskal算法:
- 策略:合并森林,按边权从小到大选择
- 复杂度: O ( m log n ) O(m \log n) O(mlogn)
- 适用:稀疏图
-
Prim算法:
- 策略:生长树,从单个顶点开始
- 复杂度: O ( n 2 ) O(n^2) O(n2)(数组)或 O ( m log n ) O(m \log n) O(mlogn)(堆)或 O ( n log n + m ) O(n \log n + m) O(nlogn+m)(斐波那契堆)
- 适用:稠密图(数组)或稀疏图(堆)
-
算法比较:
- 相同点:都使用贪心策略,都基于通用框架
- 不同点:集合 A A A的性质、选择策略、数据结构、复杂度不同
知识地图:
关键决策点:
- 何时使用Kruskal:稀疏图,边已经排序,实现简单
- 何时使用Prim:稠密图(数组),需要在线算法,与Dijkstra类似
- 如何优化Kruskal:使用高效的Union-Find数据结构
- 如何优化Prim:根据图的特点选择合适的数据结构(数组/堆/斐波那契堆)
💡 延伸学习:最小生成树算法是图算法的基础,掌握它们有助于我们:
- 理解网络设计问题(如通信网络、电路设计)
- 解决实际的最优化问题
- 为学习更复杂的图算法打下基础
2万+

被折叠的 条评论
为什么被折叠?



