Kruskal 最小生成树:边按造价排序 + 并查集贪心选边,最后按 (u,v) 字典序输出。
OJ: roj
题目 ID: 1348
难度:普及-
标签:最小生成树Kruskal贪心并查集图论python
创建: 2026-09-30 06:23
更新: 2026-10-07 13:50
前置题目
luogu P1536村村通B 的 Kruskal 主循环在 B 的代码里逐行指认了 A 教的这一步:find(u)、find(v) 后「两端代表元不同才合并」(father[ru]=rv,main.py 第 61-63 行),A 只教了并查集判连通与合并这一层,B 在它外面叠加按权排序、贪心选满 n-1 条边和复现 std 快排。luogu P1551亲戚B 的 Kruskal 把 A 教的「find 取代表元并比较是否同块」直接用作选边判据:代码里 `ru, rv = find(father,u), find(father,v)` 后 `if ru != rv: father[ru] = rv` 才选边,只是在此外层叠加按权排序、割性质贪心与复现 std 不稳定快排的输出处理。luogu P3367【模板】并查集B 的 Kruskal 直接用 A 教的 find 找代表元判同集合这一步来决定每条边是否选中(代码 ru!=rv 才 father[ru]=rv),并沿用 A 的路径压缩,只是在外层叠加按权排序、割性质贪心与复现 std 不稳定快排的输出要求。
题目描述
给定 n 个城市(1<n≤100)和 e 条无向边,每条边给出两个端点和修建造价。要求选出 n-1 条边把所有城市连通且总造价最少——即最小生成树,输出这 n-1 条边的两个端点(小端在前)。样例:5 个城市 8 条候选边,最小生成树为 1-2 / 2-3 / 3-4 / 3-5,总造价 19。
思路
Kruskal 模板:把全部边按造价从小到大排序,依次扫描,若两端未连通就用并查集把两个连通块合并;选满 n-1 条边即停。最后把选中的边按端点 (u,v) 字典序排序输出。
参考代码
Python 版:
python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-28 17:05
# update_at: 2026-09-28 17:05
import sys
def quick_sort(edges: list[tuple[int, int, int]], left: int, right: int) -> None:
"""手写快速排序(挖坑法)。
本题正确解只要求"任意最小生成树",但官方数据由本题 std 的手写快排生成,
同权边大量存在,快排的不稳定交换决定了选边顺序。要复现官方输出,
排序必须与 std.cpp 完全同构,故不用 sorted()。
"""
i, j = left, right
pivot = edges[(left + right) // 2][2] # 轴枢取中点元素的权
while i <= j:
while edges[i][2] < pivot:
i += 1
while edges[j][2] > pivot:
j -= 1
if i <= j:
edges[i], edges[j] = edges[j], edges[i] # 相等权也交换,不稳定性的来源
i += 1
j -= 1
if i < right:
quick_sort(edges, i, right)
if left < j:
quick_sort(edges, left, j)
def find(father: list[int], x: int) -> int:
"""路径压缩的并查集查找。"""
if father[x] == x:
return x
father[x] = find(father, father[x])
return father[x]
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n, e = next(data), next(data) # 城市数 n,边数 e
# 读入无向边,小端点归一化在前;下标从 1 起,与快排的 [1, m] 区间对齐
edges: list[tuple[int, int, int]] = [None] * (e + 1) # (u, v, w),0 号位空置
for i in range(1, e + 1):
u, v, w = next(data), next(data), next(data)
if u > v:
u, v = v, u
edges[i] = (u, v, w)
quick_sort(edges, 1, e)
father = list(range(n + 1)) # 城市编号 1..n,0 号位空置
mst: list[tuple[int, int]] = []
for i in range(1, e + 1): # 权从小到大尝试每条边
u, v, _ = edges[i]
ru, rv = find(father, u), find(father, v)
if ru != rv: # 不在同一连通块 → 选中
father[ru] = rv
mst.append((u, v))
if len(mst) == n - 1: # 生成树已成形,提前收工
break
mst.sort() # 按 (u, v) 字典序输出
print("\n".join(f"{u} {v}" for u, v in mst))
if __name__ == "__main__":
solve()C++ 版(同一算法):
cpp
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-05 11:15
* update_at: 2026-10-05 11:15
*/
// main.cpp:Kruskal 求最小生成树,输出选中的 n-1 条边(端点小端在前)。
#include <iostream>
#include <algorithm>
#include <cstdio>
using namespace std;
typedef long long ll;
const int MAXN = 105;
const int MAXM = 10005;
// father[i] 表示城市 i 所在并查集的根;0 号位空置。
int father[MAXN];
struct Edge {
int u, v; // 两个端点(读入时归一化 u <= v)
int w; // 造价
};
Edge edges[MAXM]; // 1..e 存边
int n, e;
// 并查集 find:沿父指针找根,路径压缩让后续查询近似 O(1)。
int find_set(int x) {
if (father[x] == x) return x;
father[x] = find_set(father[x]);
return father[x];
}
void read_input() {
scanf("%d %d", &n, &e);
for (int i = 1; i <= e; i++) {
int u, v, w;
scanf("%d %d %d", &u, &v, &w);
if (u > v) {
int t = u; u = v; v = t;
}
edges[i].u = u;
edges[i].v = v;
edges[i].w = w;
}
}
// 按造价从小到大排序(升序);造价相同则按 u 升序,再按 v 升序,保证输出去重。
bool cmp_edge(const Edge &a, const Edge &b) {
if (a.w != b.w) return a.w < b.w;
if (a.u != b.u) return a.u < b.u;
return a.v < b.v;
}
// 已选边集合,最后按 (u, v) 字典序输出
Edge mst[MAXN];
int mst_cnt;
// 按 (u, v) 字典序比较,用于最后输出排序
bool cmp_uv(const Edge &a, const Edge &b) {
if (a.u != b.u) return a.u < b.u;
return a.v < b.v;
}
void solve() {
sort(edges + 1, edges + e + 1, cmp_edge);
// 初始化并查集:每个城市各自为根
for (int i = 1; i <= n; i++) father[i] = i;
mst_cnt = 0;
// 按造价从小到大扫描每条边:两端不在同一集合则选它并合并
for (int i = 1; i <= e && mst_cnt < n - 1; i++) {
int u = edges[i].u, v = edges[i].v;
int ru = find_set(u), rv = find_set(v);
if (ru != rv) {
father[ru] = rv;
mst[++mst_cnt] = edges[i];
}
}
// 输出按 (u, v) 字典序排序,与官方输出一致
sort(mst + 1, mst + mst_cnt + 1, cmp_uv);
for (int i = 1; i <= mst_cnt; i++) {
printf("%d %d\n", mst[i].u, mst[i].v);
}
}
int main() {
read_input();
solve();
return 0;
}