ruye07

导航

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 算法 ===========

"""
核心思想:从边出发

  1. 所有边按权重从小到大排序
  2. 依次扫描每条边,用并查集判断两端是否已连通
  3. 不连通就选这条边,连通就跳过
  4. 选够 n-1 条边就停

时间复杂度:O(m log m)(排序)
空间复杂度:O(n + m)
适用:稀疏图(边少)
image

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 算法 ============

核心思想:从点出发

  1. 从任意起点开始,维护一个"已连通集合"
  2. 用优先队列(堆)维护:从集合内连到集合外的最便宜边
  3. 每次弹出堆顶,如果对面点没访问过就拉进来,同时更新堆
  4. 这就是你问的"动态更新最优连接边"!
用白话说就是找一个点,然后选出花费最少的路径,然后在原有通路上再选出花费最少的路径,使解锁新的点

image

时间复杂度: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 元组按第一个元素比较,所以放 (权重, 点)

posted on 2026-05-12 15:57  ruye07  阅读(21)  评论(0)    收藏  举报