1. 从实际问题理解最小生成树的价值
想象你是一位城市规划师,负责在五个新开发的居民区之间铺设供水管道。每个居民区之间铺设管道的成本各不相同(如下图),如何用最低的总成本让所有居民区都能通水?
code复制居民区A --8-- 居民区B
| \ / |
| 5 2 |
6 \ / 3
| 居民区C |
| / \ |
4 7 1
|/ \|
居民区D --9-- 居民区E
这就是典型的最小生成树(Minimum Spanning Tree, MST)问题。我们需要选择一组边,连接所有顶点(居民区),且这些边的权重总和最小。Kruskal算法正是解决这类问题的经典方法之一,由Joseph Kruskal在1956年提出。
最小生成树在实际应用中无处不在:电网布线、交通网络规划、通信光缆部署、芯片设计中的引脚连接等。其核心价值在于用最优成本实现全局连通性。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Kruskal算法核心思想解析
2.1 贪心策略的直观理解
Kruskal算法采用贪心策略(Greedy Approach),每次选择当前未被选中的、权重最小的边,如果这条边不会与已选边形成环路,就将其加入生成树。这种"局部最优导致全局最优"的特性,使得算法效率极高。
用生活场景类比:假设你要用最少的钱请全班同学吃火锅。Kruskal的做法是:
- 列出所有可能的两人组合及AA制费用
- 从最便宜的组合开始邀请
- 确保每次新邀请不会让同一个人出现在多个已付费组合中
- 直到所有人都被包含至少一次
2.2 算法伪代码实现
python复制KRUSKAL(G):
A = ∅
for each vertex v in G.V:
MAKE-SET(v)
sort the edges of G.E by weight in non-decreasing order
for each edge (u, v) in G.E:
if FIND-SET(u) ≠ FIND-SET(v):
A = A ∪ {(u, v)}
UNION(u, v)
return A
关键操作解析:
MAKE-SET(v): 为每个顶点创建独立的集合FIND-SET(v): 查找顶点所属集合UNION(u, v): 合并两个顶点所在的集合
3. 完整实现与复杂度分析
3.1 C++实现示例
cpp复制#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct Edge {
int src, dest, weight;
};
struct Graph {
int V, E;
vector<Edge> edges;
};
struct subset {
int parent;
int rank;
};
int find(subset subsets[], int i) {
if (subsets[i].parent != i)
subsets[i].parent = find(subsets, subsets[i].parent);
return subsets[i].parent;
}
void Union(subset subsets[], int x, int y) {
int xroot = find(subsets, x);
int yroot = find(subsets, y);
if (subsets[xroot].rank < subsets[yroot].rank)
subsets[xroot].parent = yroot;
else if (subsets[xroot].rank > subsets[yroot].rank)
subsets[yroot].parent = xroot;
else {
subsets[yroot].parent = xroot;
subsets[xroot].rank++;
}
}
bool compareEdges(Edge a, Edge b) {
return a.weight < b.weight;
}
void KruskalMST(Graph graph) {
int V = graph.V;
Edge result[V];
int e = 0, i = 0;
sort(graph.edges.begin(), graph.edges.end(), compareEdges);
subset *subsets = new subset[V];
for (int v = 0; v < V; v++) {
subsets[v].parent = v;
subsets[v].rank = 0;
}
while (e < V - 1 && i < graph.E) {
Edge next_edge = graph.edges[i++];
int x = find(subsets, next_edge.src);
int y = find(subsets, next_edge.dest);
if (x != y) {
result[e++] = next_edge;
Union(subsets, x, y);
}
}
cout << "Edges in MST:\n";
for (i = 0; i < e; i++)
cout << result[i].src << " -- " << result[i].dest
<< " : " << result[i].weight << endl;
delete[] subsets;
}
3.2 时间复杂度详解
Kruskal算法的性能主要取决于:
- 边排序:O(E log E) —— 使用快速排序或归并排序
- 并查集操作:
- FIND-SET: 接近O(1)(使用路径压缩)
- UNION: 接近O(1)(使用按秩合并)
总时间复杂度为O(E log E + E α(V)),其中α是反阿克曼函数,增长极其缓慢,可视为常数。因此通常简化为O(E log E)或O(E log V)(因为log E < log V² = 2 log V)。
实际工程中,当E接近V²时(完全图),Prim算法可能更优;但对于稀疏图(E≈V),Kruskal通常是更好的选择。
4. 实战应用与边界处理
4.1 居民区供水问题解决方案
回到开头的居民区问题,我们首先列出所有边及其权重:
code复制边 权重
A-B 8
A-C 5
A-D 6
B-C 2
B-E 3
C-D 7
C-E 1
D-E 9
按照Kruskal算法执行步骤:
- 排序边:C-E(1), B-C(2), B-E(3), A-C(5), A-D(6), C-D(7), A-B(8), D-E(9)
- 依次选择:
- 选C-E(不形成环)
- 选B-C(不形成环)
- 选B-E → 会形成B-C-E-B环,跳过
- 选A-C(不形成环)
- 选A-D(不形成环)
- 此时已连接所有顶点(边数=顶点数-1=4),算法终止
最终MST包含:C-E(1), B-C(2), A-C(5), A-D(6),总成本=14
4.2 常见边界情况处理
-
图不连通:当算法结束时选择的边数 < V-1,说明图不连通
cpp复制if (e < V - 1) cout << "Graph is disconnected! No MST exists." << endl; -
重复边权重:排序时需稳定排序,或添加次要比较条件
cpp复制bool compareEdges(Edge a, Edge b) { if (a.weight != b.weight) return a.weight < b.weight; return a.src < b.src; // 次要排序条件 } -
自环边:应在预处理阶段移除,因其不会出现在MST中
cpp复制graph.edges.erase( remove_if(graph.edges.begin(), graph.edges.end(), [](Edge e) { return e.src == e.dest; }), graph.edges.end());
5. 与Prim算法的对比选型
5.1 性能对比表
| 特性 | Kruskal算法 | Prim算法 |
|---|---|---|
| 适用图类型 | 稀疏图(E≈V) | 稠密图(E≈V²) |
| 时间复杂度 | O(E log E) | O(V²)(邻接矩阵) |
| O(E log V)(二叉堆) | ||
| 存储结构 | 边列表 | 邻接矩阵/邻接表 |
| 并行化潜力 | 高(边排序可并行) | 低 |
| 实现复杂度 | 中等(需并查集) | 简单 |
5.2 选型建议
-
选择Kruskal当:
- 图以边列表形式存储
- 边数量远小于V²(稀疏图)
- 需要并行处理大规模数据
- 边权重经常动态变化(只需重新排序)
-
选择Prim当:
- 图以邻接矩阵形式存储
- 图非常稠密(E接近V²)
- 需要频繁查询特定顶点的邻边
- 从特定顶点开始构建树很重要
6. 工程实践中的优化技巧
6.1 并查集优化实战
并查集的两种关键优化极大提升性能:
-
路径压缩:使查找路径上的所有节点直接指向根
cpp复制int find(subset subsets[], int i) { if (subsets[i].parent != i) subsets[i].parent = find(subsets, subsets[i].parent); // 递归压缩 return subsets[i].parent; } -
按秩合并:总是将较矮的树合并到较高的树下
cpp复制void Union(subset subsets[], int x, int y) { // ... (见前面实现) if (subsets[xroot].rank < subsets[yroot].rank) subsets[xroot].parent = yroot; else if (subsets[xroot].rank > subsets[yroot].rank) subsets[yroot].parent = xroot; else { subsets[yroot].parent = xroot; subsets[xroot].rank++; // 仅当秩相等时增加 } }
6.2 内存访问优化
现代CPU缓存对性能影响巨大,可以:
-
紧凑存储边数据:用结构体数组而非类对象
cpp复制struct Edge { int src, dest, weight; } __attribute__((packed)); // 避免内存对齐空隙 -
预分配内存:避免动态扩容开销
cpp复制vector<Edge> edges; edges.reserve(E); // 预先分配足够空间 -
批量处理边:利用缓存局部性
cpp复制const int BATCH_SIZE = 1024; for (int i = 0; i < graph.E; i += BATCH_SIZE) { int end = min(i + BATCH_SIZE, graph.E); sort(edges.begin() + i, edges.begin() + end, compareEdges); }
7. 典型应用场景扩展
7.1 聚类分析
Kruskal算法可应用于层次聚类:
- 将每个数据点视为顶点
- 计算所有点对之间的距离作为边权重
- 运行Kruskal直到剩下k个连通分量
- 每个连通分量就是一个聚类
python复制def kruskal_clustering(points, k):
# 计算所有边
edges = []
for i in range(len(points)):
for j in range(i+1, len(points)):
dist = euclidean_distance(points[i], points[j])
edges.append((i, j, dist))
# Kruskal算法变种
edges.sort(key=lambda x: x[2])
parent = [i for i in range(len(points))]
clusters = len(points)
for u, v, d in edges:
if clusters <= k:
break
root_u = find(parent, u)
root_v = find(parent, v)
if root_u != root_v:
parent[root_v] = root_u
clusters -= 1
# 提取聚类结果
clusters = {}
for i in range(len(points)):
root = find(parent, i)
clusters.setdefault(root, []).append(points[i])
return list(clusters.values())
7.2 网络设计优化
在设计分布式系统的通信网络时:
- 将数据中心作为顶点
- 链路延迟或带宽成本作为边权重
- 使用Kruskal找出最优连接方案
- 可扩展为多级树形网络
java复制public class DataCenterNetwork {
class Link implements Comparable<Link> {
int dc1, dc2;
double latency; // 或 1/bandwidth
public int compareTo(Link other) {
return Double.compare(this.latency, other.latency);
}
}
public List<Link> designNetwork(List<DataCenter> dcs, List<Link> links) {
Collections.sort(links);
UnionFind uf = new UnionFind(dcs.size());
List<Link> mst = new ArrayList<>();
for (Link link : links) {
if (uf.find(link.dc1) != uf.find(link.dc2)) {
uf.union(link.dc1, link.dc2);
mst.add(link);
if (mst.size() == dcs.size() - 1) break;
}
}
return mst;
}
}
8. 算法变种与进阶思考
8.1 次小生成树
在已求得MST的基础上,次小生成树可以通过以下步骤求得:
- 对MST中的每条边e:
- 从原图中移除e
- 在原图中寻找连接e两端点的最小边e'
- 计算新生成树的权重 = MST权重 - e.weight + e'.weight
- 取所有可能中的最小值
python复制def second_mst(graph, mst_edges):
min_weight = sum(e[2] for e in mst_edges)
second_min = float('inf')
for e in mst_edges:
# 临时移除当前边
temp_graph = [edge for edge in graph if edge != e]
# 找到连接e.u和e.v的最小边
min_replacement = float('inf')
for edge in temp_graph:
if (edge[0] == e[0] and edge[1] == e[1]) or \
(edge[0] == e[1] and edge[1] == e[0]):
if edge[2] < min_replacement:
min_replacement = edge[2]
if min_replacement != float('inf'):
candidate = min_weight - e[2] + min_replacement
if candidate < second_min:
second_min = candidate
return second_min
8.2 并行化Kruskal算法
对于超大规模图(如社交网络分析),可并行化处理:
-
边排序阶段:
- 将边分块并行排序(如使用并行归并排序)
- 然后合并排序结果
-
并查集操作阶段:
- 使用并发并查集数据结构
- 采用乐观并发控制或细粒度锁
cpp复制// 伪代码示例 - 并行边排序
void parallel_kruskal(Graph &graph) {
int num_threads = omp_get_max_threads();
int chunk_size = graph.E / num_threads;
#pragma omp parallel for
for (int i = 0; i < num_threads; i++) {
int start = i * chunk_size;
int end = (i == num_threads-1) ? graph.E : start + chunk_size;
sort(graph.edges.begin()+start, graph.edges.begin()+end, compareEdges);
}
// 并行合并排序结果
vector<Edge> sorted_edges;
// ... 实现多路归并 ...
// 并行处理边 (需要线程安全的并查集)
#pragma omp parallel for
for (int i = 0; i < sorted_edges.size(); i++) {
Edge &e = sorted_edges[i];
lock(e.src); lock(e.dest);
if (find(e.src) != find(e.dest)) {
union_sets(e.src, e.dest);
mst_edges.push_back(e);
}
unlock(e.dest); unlock(e.src);
}
}
9. 可视化理解与调试技巧
9.1 分步可视化示例
以5个顶点的图为例,演示Kruskal执行过程:
code复制初始图:
A -2- B -3- C
| / | /
4 3 1 4
| / | /
D -5- E
执行步骤:
| 步骤 | 选中边 | 权重 | 并查集状态 | 操作 |
|---|---|---|---|---|
| 1 | B-E | 1 | {A}{B,E}{C} | 合并B,E |
| 2 | A-B | 2 | {A,B,E}{C} | 合并A,B |
| 3 | B-C | 3 | {A,B,C,E} | 合并B,C |
| 4 | A-D | 4 | 会形成A-B-D-A环 | 跳过 |
| 5 | C-E | 4 | 会形成B-C-E-B环 | 跳过 |
| 6 | D-E | 5 | 合并D,E |
最终MST:B-E(1), A-B(2), B-C(3), D-E(5)
9.2 调试技巧
-
验证无环性:每次添加边后,检查整个图的连通分量数量
python复制def has_cycle(parent, u, v): return find(parent, u) == find(parent, v) -
权重和验证:定期计算已选边权重和,确保符合预期
cpp复制int total = 0; for (Edge e : mst_edges) { total += e.weight; assert(find(parent, e.src) != find(parent, e.dest)); } -
可视化工具:使用Graphviz生成中间状态图
python复制def visualize(graph, mst_edges, step): dot = Digraph() for u, v, w in graph.edges: color = 'red' if (u,v,w) in mst_edges else 'black' dot.edge(str(u), str(v), label=str(w), color=color) dot.render(f'step_{step}', view=True)
10. 常见问题与解决方案
10.1 为什么我的Kruskal实现比Prim慢?
可能原因及解决:
-
边排序开销大:尝试:
- 使用更快的排序算法(如内省排序)
- 对权重使用基数排序(如果权重范围小)
cpp复制// 当权重是0-1000的整数时: void counting_sort_edges(vector<Edge> &edges) { vector<vector<Edge>> buckets(1001); for (Edge e : edges) buckets[e.weight].push_back(e); edges.clear(); for (auto &bucket : buckets) edges.insert(edges.end(), bucket.begin(), bucket.end()); } -
并查集实现不佳:
- 确保使用了路径压缩和按秩合并
- 考虑使用内存更紧凑的数据结构
-
图存储方式不当:
- 对于静态图,使用边数组而非邻接表
- 确保内存访问模式是连续的
10.2 如何处理浮点权重?
浮点比较需要特殊处理:
cpp复制bool compareEdges(Edge a, Edge b) {
if (fabs(a.weight - b.weight) > 1e-9)
return a.weight < b.weight;
// 次要排序条件确保稳定性
if (a.src != b.src) return a.src < b.src;
return a.dest < b.dest;
}
10.3 大规模图的存储优化
当图无法完全装入内存时:
- 外部排序:对边进行磁盘上的归并排序
- 分块处理:将图分成多个块,分别处理后再合并
- 概率算法:使用随机抽样估计MST
python复制def external_kruskal(edge_file, num_vertices):
# 第一阶段:外部排序
os.system(f"sort -n -k3,3 {edge_file} > sorted_edges.txt")
# 第二阶段:逐步处理
parent = [i for i in range(num_vertices)]
mst = []
with open("sorted_edges.txt") as f:
for line in f:
u, v, w = map(int, line.strip().split())
root_u = find(parent, u)
root_v = find(parent, v)
if root_u != root_v:
parent[root_v] = root_u
mst.append((u, v, w))
if len(mst) == num_vertices - 1:
break
return mst
11. 从Kruskal到更高级图算法
11.1 扩展到有向图:Edmonds算法
对于有向图的最小生成树(称为最小树形图),可以使用Chu-Liu/Edmonds算法,其核心思想包含类似Kruskal的贪心选择:
- 为每个非根节点选择最小入边
- 如果无环,即为解;否则收缩环为一个超级节点
- 递归处理新图
python复制def edmonds(vertices, edges, root):
# 步骤1:选择最小入边
min_in = {v: None for v in vertices if v != root}
for u, v, w in edges:
if v != root and (min_in[v] is None or w < min_in[v][2]):
min_in[v] = (u, v, w)
# 检查是否有环
visited = {}
cycles = []
for v in vertices:
if v not in visited and v != root:
path = []
current = v
while current is not None and current not in visited:
visited[current] = len(path)
path.append(current)
if min_in[current] is None:
current = None
else:
current = min_in[current][0]
if current is not None and current in visited:
cycle = path[visited[current]:]
cycles.append(cycle)
if not cycles:
return [e for e in min_in.values() if e is not None]
# 处理环
cycle = cycles[0]
super_node = max(vertices) + 1
# 构建新图
new_edges = []
for u, v, w in edges:
if u in cycle and v in cycle:
continue
elif v in cycle:
new_edges.append((u, super_node, w - min_in[v][2]))
elif u in cycle:
new_edges.append((super_node, v, w))
else:
new_edges.append((u, v, w))
# 递归求解
mst = edmonds([v for v in vertices if v not in cycle] + [super_node],
new_edges, root)
# 展开超级节点
result = []
in_edge = None
for u, v, w in mst:
if v == super_node:
# 找到原图中对应的边
for orig_u, orig_v, orig_w in edges:
if orig_v in cycle and orig_w == w + min_in[orig_v][2]:
in_edge = (orig_u, orig_v, orig_w)
break
result.append(in_edge)
else:
result.append((u, v, w))
# 添加环中其他边
for v in cycle:
if min_in[v] != in_edge:
result.append(min_in[v])
return result
11.2 动态图场景处理
当图的边可能动态增减时,需要动态MST算法。基于Kruskal的思路可以这样扩展:
- 初始状态:维护已排序的边列表和并查集
- 添加边:
- 二分查找插入位置保持有序性
- 如果边连接不同连通分量,加入MST
- 删除边:
- 如果边不在MST中,直接移除
- 否则,在剩余边中寻找替代边
java复制class DynamicMST {
TreeSet<Edge> edges = new TreeSet<>();
UnionFind uf;
List<Edge> mst = new ArrayList<>();
public void addEdge(Edge e) {
if (edges.contains(e)) return;
edges.add(e);
int rootU = uf.find(e.u);
int rootV = uf.find(e.v);
if (rootU != rootV) {
uf.union(e.u, e.v);
mst.add(e);
}
}
public void removeEdge(Edge e) {
if (!edges.contains(e)) return;
edges.remove(e);
if (!mst.contains(e)) return;
// 需要重新连接两个连通分量
mst.remove(e);
uf = new UnionFind(uf.size());
for (Edge mstEdge : mst) {
uf.union(mstEdge.u, mstEdge.v);
}
// 寻找替代边
for (Edge candidate : edges) {
if (candidate == e) continue;
if (uf.find(candidate.u) != uf.find(candidate.v)) {
uf.union(candidate.u, candidate.v);
mst.add(candidate);
break;
}
}
}
}
12. 数学证明与正确性讨论
12.1 贪心选择性质的证明
Kruskal算法的正确性基于以下定理:
定理:设G=(V,E)是连通无向图,对任意非空边子集S⊆E,如果S是某棵MST的一部分,且S∪{e}也不含环,则存在一棵包含S∪{e}的MST。
证明:
- 假设T是一棵包含S的MST,且e∉T
- 将e加入T会形成唯一一个环C
- 环C上必存在另一条边f∈T但f∉S(因为S∪{e}无环)
- 由于Kruskal按权重排序选择e,有w(e)≤w(f)
- 用e替换f得到T'=T∪{e}{f},且w(T')≤w(T)
- 因此T'也是MST且包含S∪
12.2 与拟阵理论的联系
Kruskal算法是拟阵(Matroid)贪心算法的特例:
-
图拟阵:M=(E,I),其中I为无环边集
- 遗传性:子集的无环性可遗传
- 交换性:若A,B∈I且|A|<|B|,则存在e∈B\A使A∪{e}∈I
-
权函数:边权重w:E→ℝ⁺
-
Kruskal本质:在图拟阵上寻找最大权独立集(即无环边集),等价于寻找最小权生成树
math复制\text{MST} = \arg\min_{T \subseteq E, \text{spanning tree}} \sum_{e \in T} w(e) = \arg\max_{T \subseteq E, \text{spanning tree}} \sum_{e \in E \setminus T} w(e)
13. 现代优化与变种算法
13.1 Filter-Kruskal算法
针对稀疏图的优化版本,核心思想:
- 在排序前先过滤掉明显不会进入MST的边
- 使用分治策略减少实际需要排序的边数
python复制def filter_kruskal(edges, num_vertices):
if len(edges) <= 1:
return edges
pivot = median([e[2] for e in edges])
light_edges = [e for e in edges if e[2] <= pivot]
heavy_edges = [e for e in edges if e[2] > pivot]
uf = UnionFind(num_vertices)
mst = []
# 递归处理轻边
mst += filter_kruskal(light_edges, num_vertices)
# 更新并查集
for u, v, w in mst:
uf.union(u, v)
# 过滤重边
filtered_heavy = []
for u, v, w in heavy_edges:
if uf.find(u) != uf.find(v):
filtered_heavy.append((u, v, w))
mst += filter_kruskal(filtered_heavy, num_vertices)
return mst
13.2 随机化Kruskal
通过随机采样加速:
- 随机选择部分边构建初始森林
- 在剩余边中运行标准Kruskal
- 理论复杂度可降至O(E + V log V)
cpp复制vector<Edge> randomized_kruskal(vector<Edge> edges, int V) {
random_shuffle(edges.begin(), edges.end());
UnionFind uf(V);
vector<Edge> mst;
// 先处理前k条边
int k = V * log2(V);
for (int i = 0; i < min(k, (int)edges.size()); i++) {
Edge &e = edges[i];
if (uf.find(e.src) != uf.find(e.dest)) {
uf.union(e.src, e.dest);
mst.push_back(e);
}
}
// 标准Kruskal处理剩余边
sort(edges.begin() + k, edges.end(), compareEdges);
for (int i = k; i < edges.size(); i++) {
Edge &e = edges[i];
if (uf.find(e.src) != uf.find(e.dest)) {
uf.union(e.src, e.dest);
mst.push_back(e);
}
}
return mst;
}
14. 跨语言实现对比
14.1 Python实现特点
python复制def kruskal(edges, num_vertices):
parent = list(range(num_vertices))
def find(u):
while parent[u] != u:
parent[u] = parent[parent[u]] # 路径压缩
u = parent[u]
return u
edges.sort(key=lambda x: x[2])
mst = []
for u, v, w in edges:
root_u, root_v = find(u), find(v)
if root_u != root_v:
parent[root_v] = root_u # 简单合并,无按秩优化
mst.append((u, v, w))
if len(mst) == num_vertices - 1:
break
return mst
特点:
- 利用lambda简化比较
- 动态类型减少代码量
- 缺少按秩合并优化
- 适合快速原型开发
14.2 Java实现考量
java复制public class Kruskal {
static class Edge implements Comparable<Edge> {
int src, dest, weight;
public int compareTo(Edge other) {
return Integer.compare(this.weight, other.weight);
}
}
static class UnionFind {
int[] parent, rank;
UnionFind(int n) {
parent = new int[n];
rank = new int[n];
for (int i = 0; i < n; i++)
parent[i] = i;
}
int find(int u) {
if (parent[u] != u)
parent[u] = find(parent[u]);
return parent[u];
}
void union(int u, int v) {
int rootU = find(u), rootV = find(v);
if (rootU == rootV) return;
if (rank[rootU] > rank[rootV])
parent[rootV] = rootU;
else {
parent[rootU] = rootV;
if (rank[rootU] == rank[rootV])
rank[rootV]++;
}
}
}
public static List<Edge> kruskal(List<Edge> edges, int numVertices) {
Collections.sort(edges);
UnionFind uf = new UnionFind(numVertices);
List<Edge> mst = new ArrayList<>();
for (Edge e : edges) {
if (mst.size() == numVertices - 1) break;
if (uf.find(e.src) != uf.find(e.dest)) {
uf.union(e.src, e.dest);
mst.add(e);
}
}
return mst;
}
}
特点:
- 严格的类型系统
- 完整的并查集优化
- 面向对象封装
- 适合大型工程化项目
15. 测试用例设计与验证
15.1 单元测试样例
完整测试应包含以下场景:
-
基本连通图:
python复制edges = [ (0, 1, 4), (0, 2, 3), (1, 2, 1), (1, 3, 2), (2, 3, 5), (3, 4, 6) ] mst = kruskal(edges, 5) assert sum(w for _, _, w in mst) == 12 -
不连通图:
python复制edges = [(0,1,1), (2,3,2)] try: mst = kruskal(edges, 4) assert False, "Should raise error" except ValueError as e: assert str(e) == "Graph is disconnected" -
重复权重:
python复制edges = [(0,1,1), (1,2,1), (0,2,2)] mst = kruskal(edges, 3) assert len(mst) == 2 # 两种合法MST之一 -
大规模随机图:
python复制import random random.seed(42) edges = [] for i in range(100): for j in range(i+1, 100): if random.random() < 0.1: # 10%密度 edges.append((i, j, random.randint(1,100))) mst = kruskal(edges, 100) assert len(mst) == 99
15.2 正确性验证方法
-
MST属性验证:
- 边数 = 顶点数 - 1
- 图保持连通
- 无环
- 权重和最小
-
对比验证:
python复制def prim(edges, num_vertices): # Prim算法实现 pass edges = generate_random_graph(50, 0.2) mst_kruskal = kruskal(edges, 50) mst_prim = prim(edges, 50) assert sum(w for _,_,w in mst_kruskal) == sum(w for _,_,w in mst_prim) -
**极端情况
