最小生成树(Minimum Spanning Tree)

最小生成树(Minimum Spanning Tree,MST)是图论中的经典问题:在一个带权无向图中,找到连接所有顶点且总权重最小的树。

什么是最小生成树?

生成树:连接图中所有顶点且无环的边集。

最小生成树:总权重最小的那个生成树。

原始图:          最小生成树:
    1                    1
   /|\                  /|
  2 3 4                2 |
 /  \|/ \                \|
5----6----7            5--6--7

总权重: 最小           总权重: 最小

MST 的性质

  1. 边的数量 = 顶点数 - 1
  2. 不存在环
  3. 总权重最小

应用场景

  • 网络布线(光缆、网线)
  • 电路板设计
  • 城市道路规划
  • 聚类分析

算法一:Kruskal 算法

核心思想

贪心:每次选最短的边,只要不形成环就加入。

Kruskal 步骤:
1. 把所有边按权重从小到大排序
2. 依次遍历每条边:
   - 如果这条边连接的两个顶点不在同一个集合 → 加入
   - 否则 → 跳过(会形成环)
3. 直到选了 n-1 条边

实现(并查集)

class UnionFind:
    def __init__(self, n):
        self.parent = list(range(n))
        self.rank = [0] * n
    
    def find(self, x):
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])  # 路径压缩
        return self.parent[x]
    
    def union(self, x, y):
        px, py = self.find(x), self.find(y)
        if px == py:
            return False  # 已在同一集合
        # 按秩合并
        if self.rank[px] < self.rank[py]:
            px, py = py, px
        self.parent[py] = px
        if self.rank[px] == self.rank[py]:
            self.rank[px] += 1
        return True

def kruskal(n, edges):
    """
    n: 顶点数
    edges: [(u, v, w), ...] 边列表
    """
    # 1. 按权重排序
    edges.sort(key=lambda x: x[2])
    
    # 2. 初始化并查集
    uf = UnionFind(n)
    
    mst_weight = 0
    mst_edges = []
    edge_count = 0
    
    for u, v, w in edges:
        if uf.union(u, v):
            mst_weight += w
            mst_edges.append((u, v, w))
            edge_count += 1
            if edge_count == n - 1:
                break
    
    return mst_weight, mst_edges

时间复杂度

步骤 时间复杂度
排序所有边 O(E log E)
并查集操作 O(E · α(N)) ≈ O(E)
总复杂度 O(E log E)

E = 边数,α = Ackermann 函数(近似常数)


算法二:Prim 算法

核心思想

贪心 + 切分:从一个顶点开始,每次选择跨越”已选集合”和”未选集合”的最小边。

Prim 步骤:
1. 任选一个起始顶点,加入已选集合
2. 重复直到所有顶点都被选中:
   - 找到连接已选集合和未选集合的最小边
   - 将该边的另一顶点加入已选集合

实现(堆优化)

import heapq

def prim(n, edges):
    """
    n: 顶点数
    edges: [(u, v, w), ...] 边列表
    """
    # 建邻接表
    graph = [[] for _ in range(n)]
    for u, v, w in edges:
        graph[u].append((v, w))
        graph[v].append((u, w))
    
    # 从顶点 0 开始
    visited = [False] * n
    visited[0] = True
    
    # 候选边堆:(权重, 目标顶点, 来源顶点)
    heap = [(w, v, u) for u, v, w in graph[0]]
    heapq.heapify(heap)
    
    mst_weight = 0
    edge_count = 0
    
    while heap and edge_count < n - 1:
        w, v, u = heapq.heappop(heap)
        
        if visited[v]:
            continue
        
        visited[v] = True
        mst_weight += w
        edge_count += 1
        
        # 添加新顶点相邻的边
        for nv, nw in graph[v]:
            if not visited[nv]:
                heapq.heappush(heap, (nw, nv, v))
    
    return mst_weight

时间复杂度

实现方式 时间复杂度
朴素实现 O(V²)
堆优化 O(E log V)
斐波那契堆 O(E + V log V)

LeetCode 实战

题目 1:找到最小生成树中的第 K 小的边

LeetCode 1482 - 制作 m 束花的最少天数(变形)

其实 MST 直接题不多,更多是最小连接成本的应用:

题目 2:连接n个城市的最少成本

LeetCode 1584 - 连接所有点的最小费用

class Solution:
    def minCostConnectPoints(self, points):
        """曼哈顿距离作为边的权重"""
        n = len(points)
        edges = []
        
        # 构建所有边(优化:只用相邻点)
        # 但为了通用性,这里用完全图
        for i in range(n):
            for j in range(i + 1, n):
                dist = abs(points[i][0] - points[j][0]) + \
                       abs(points[i][1] - points[j][1])
                edges.append((i, j, dist))
        
        # Kruskal
        edges.sort(key=lambda x: x[2])
        uf = UnionFind(n)
        
        total = 0
        count = 0
        
        for u, v, w in edges:
            if uf.union(u, v):
                total += w
                count += 1
                if count == n - 1:
                    break
        
        return total

题目 3:最小连接成本(变形)

LeetCode 1167 - 连接棒料棒的最小成本

class Solution:
    def connectSticks(self, sticks):
        """哈夫曼思想:用最小堆贪心"""
        if len(sticks) == 1:
            return 0
        
        heapq.heapify(sticks)
        total = 0
        
        while len(sticks) > 1:
            # 取最短的两根
            a = heapq.heappop(sticks)
            b = heapq.heappop(sticks)
            
            cost = a + b
            total += cost
            
            # 合并后放回堆
            heapq.heappush(sticks, cost)
        
        return total

题目 4:Kruskal vs Prim 选择

# 选择原则:
# - 稀疏图 (E ≈ V): Kruskal O(E log E) 更优
# - 稠密图 (E ≈ V²): Prim O(E log V) 更优
# - 一般用 Kruskal,更容易实现

def mst(n, edges, method='kruskal'):
    if method == 'kruskal':
        return kruskal(n, edges)
    else:  # prim
        return prim(n, edges)

两种算法对比

特性 Kruskal Prim
核心思想 选边 选点
适合图 稀疏图 稠密图
时间复杂度 O(E log E) O(E log V)
实现难度 低(并查集) 中(堆)
贪心策略 全局最短边 切分最小边

代码简洁性:Kruskal 更简单。

实际应用:大多数场景用 Kruskal,因为实际图多为稀疏。


扩展:次小生成树

次小生成树:总权重第二小的生成树。

求法

  1. 先求 MST
  2. 枚举每条非 MST 的边,加入 MST
  3. 去掉形成的环中的最大边
  4. 取最小值
def second_mst(n, edges):
    # 1. 求 MST
    mst_weight, mst_edges = kruskal(n, edges)
    mst_edge_set = set(mst_edges)
    
    # 2. 枚举非 MST 边
    result = float('inf')
    for u, v, w in edges:
        if (u, v, w) not in mst_edge_set:
            # 求 MST 中 u-v 路径上的最大边
            max_edge = max_on_path(u, v, mst_adj)
            result = min(result, mst_weight + w - max_edge)
    
    return result

💡 小结

MST 核心要点

算法 核心 时间复杂度 适合场景
Kruskal 选最短边(不形成环) O(E log E) 稀疏图
Prim 从点出发扩展 O(E log V) 稠密图

Kruskal 代码模板

def kruskal(n, edges):
    edges.sort(key=lambda x: x[2])  # 按权重排序
    uf = UnionFind(n)
    total = 0
    for u, v, w in edges:
        if uf.union(u, v):
            total += w
    return total

UnionFind 模板

class UnionFind:
    def __init__(self, n):
        self.parent = list(range(n))
        self.rank = [0] * n
    
    def find(self, x):
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]
    
    def union(self, x, y):
        px, py = self.find(x), self.find(y)
        if px == py:
            return False
        if self.rank[px] < self.rank[py]:
            px, py = py, px
        self.parent[py] = px
        if self.rank[px] == self.rank[py]:
            self.rank[px] += 1
        return True

相关阅读:线段树(Segment Tree)