Kruskal 算法 + Prim 算法 + 贪心正确性证明
"""
==================== 最小生成树 (MST) 学习笔记 ====================
学习日期:2026-05-12
包含:Kruskal 算法 + Prim 算法 + 贪心正确性证明
==================== 引导问题回顾 ====================
Q1: n个村庄修路,要全部连通且总花费最小,应该按什么顺序选边?
A: 按价格从小到大(贪心),选 n-1 条边。
Q2: 为什么要选 n-1 条边?
A: n个点的连通图最少需要 n-1 条边,这个结构就是「树」。
Q3: 扫描到一条边,两端已经在同一连通块里,还要选吗?
A: 不要——选了会成环,没必要。
Q4: 为什么按便宜选一定是最优解?不会出现"1+5 > 2+3"吗?
A: 交换论证(Exchange Argument):
- 假设最优解没选当前最便宜边 e
- 把最优解中更贵的替代边换成 e
- 连通性不变,总花费不会增加
- 所以贪心不会错过最优解
Q5: 为什么不出现"选了便宜边后面连不起来"?
A: 割性质(Cut Property):
- 把点分成两堆,跨在两堆之间最便宜的那条边
- 一定属于某棵最小生成树
- Kruskal 每一步都在某个"割"上选最便宜边,不会翻车
Q6: Kruskal vs Prim 什么时候选哪个?
A: 边多(稠密图)用 Prim(省排序开销)
边少(稀疏图)用 Kruskal(省堆操作开销)
"""
========== Kruskal 算法 ===========
"""
核心思想:从边出发
- 所有边按权重从小到大排序
- 依次扫描每条边,用并查集判断两端是否已连通
- 不连通就选这条边,连通就跳过
- 选够 n-1 条边就停
时间复杂度:O(m log m)(排序)
空间复杂度:O(n + m)
适用:稀疏图(边少)

def kruskal_template():
n, m = map(int, input().split())
# 1. 存所有边
edges = []
for _ in range(m):
u, v, w = map(int, input().split())
edges.append((w, u, v)) # w放前面,方便排序
# 2. 按边权升序排序
edges.sort()
# 3. 初始化并查集(1-indexed)
parent = list(range(n + 1))
# 路径压缩 —— 查询时把沿途节点直接挂到根上
def find(x):
if parent[x] == x:
return x
parent[x] = find(parent[x]) # 路径压缩
return parent[x]
# 合并 —— 把x的根的parent指向y的根
def union(x, y):
parent[find(x)] = find(y)
# 4. 扫描选边
ans = 0 # 总花费
cnt = 0 # 已选边数
for w, u, v in edges:
if find(u) != find(v): # 两端在不同连通块
union(u, v) # 合并
ans += w # 累计花费
cnt += 1 # 边数+1
if cnt == n - 1: # 选够了
break
# 5. 判断连通性 & 输出
print(ans if cnt == n - 1 else 'orz')
============ Prim 算法 ============
核心思想:从点出发
- 从任意起点开始,维护一个"已连通集合"
- 用优先队列(堆)维护:从集合内连到集合外的最便宜边
- 每次弹出堆顶,如果对面点没访问过就拉进来,同时更新堆
- 这就是你问的"动态更新最优连接边"!
用白话说就是找一个点,然后选出花费最少的路径,然后在原有通路上再选出花费最少的路径,使解锁新的点

时间复杂度:O(m log n)(堆操作)
空间复杂度:O(n + m)
适用:稠密图(边多)
与 Dijkstra 的区别:只差一行代码!
Prim: 堆里放 (边权, 点)
Dijkstra: 堆里放 (从起点的累计距离, 点)
import heapq
def prim_template():
n, m = map(int, input().split())
#建图 邻接表
g = [[] for _ in range(n + 1)]
for _ in range(m):
u, v, w = map(int, input().split())
g[u].append((w, v)) # (边权, 对面点)
g[v].append((w, u)) # 无向图加两条
vis = [False] * (n + 1) # 是否已加入集合
pq = [(0, 1)] # (花费, 点),从1出发,花费0
ans = 0
cnt = 0 # 已连入的点数
while pq:
w, u = heapq.heappop(pq) # 弹出当前最便宜的候选边
if vis[u]:
continue # 已经连进来了,跳过
vis[u] = True # 标记为已连通
ans += w # 累计花费
cnt += 1 # 点数+1
# 把新点的邻居加入堆(候选边)
for w2, v in g[u]:
if not vis[v]:
heapq.heappush(pq, (w2, v))
# 判断连通性 & 输出
print(ans if cnt == n else 'orz')
========== 关键概念对照表 ==========
"""
| 概念 | Kruskal | Prim |
|---|---|---|
| 视角 | 从边出发 | 从点出发 |
| 核心数据结构 | 并查集 | 堆(优先队列) |
| 贪心依据 | 全局最便宜的可连边 | 当前集合最便宜的外连边 |
| 正确性基石 | 割性质(Cut Property) | |
| 证明方法 | 交换论证(Exchange Argument) |
并查集三要素:
find(x) —— 找根 + 路径压缩
union(x,y) —— 把一方的根指向另一方
parent[x] —— 初始化为自己(每个点都是自己的根)
堆操作(heapq):
heapq.heappush(pq, item) —— 入堆
heapq.heappop(pq) —— 弹出最小值
Python 元组按第一个元素比较,所以放 (权重, 点)
浙公网安备 33010602011771号