🌱

Prim 算法:从自家田地往外扩

Kruskal 是「按桥挑」,Prim 是「从一点往外长」:先占住自家岛,每轮把「连出去最便宜」的新岛接进来——像藤蔓一样,一棵树慢慢长满全图。

💡 打个比方

上一课 Kruskal 是「全岛桥队排序,便宜优先逐座修」;Prim 换个视角:你是1 号岛的主人,想把全岛都连进自家网络。规则很简单:每轮,在所有「树里 ↔ 树外」的桥里,修最便宜的那一座,把外面的岛接进树里。树里只有一个点、两条边都不算「跨界」——只有一头在树里、一头在树外的桥才考虑。接满 n − 1 座(所有岛都进树)就停。

两种算法像「修路队」和「种田队」:Kruskal 拿着桥的价目表全图挑;Prim 守着已连通的领地一圈圈往外扩。视角不同,但都是贪心,结果一样:都是最小生成树,总造价一样低(具体可能选不同的桥,但总花费相同)。

Prim 需要一张「账本」:dist[i] = 树外的岛 i,接进树最便宜要花多少(即 i 到树里所有点的最短边权)。每接进一个新岛,就用它去「报价」:如果它到某个树外岛更便宜,就更新 dist。每轮干两件事:① 在树外找 dist 最小的点接进来;② 用它更新其余树外点的 dist。

为什么 Prim 也是最优的?同样的换桥论证:每轮接的是「跨界桥里最便宜的」,不接它就得用更贵的桥接同样的岛——贪心不亏。下一课对比两种算法的「账」:稠密图(桥多)Prim 快,稀疏图(桥少)Kruskal 快。

术语:Prim 算法(1957,从一点开始逐步扩张的贪心 MST 算法);跨界桥(一头在树里、一头在树外的边,每轮只考虑它);dist[](树外每个点到「树」的最便宜边权,Prim 的核心账本);in[](点是否已进树的标记);稠密图(边多,m 接近 n²/2,如完全图);稀疏图(边少,m 接近 n,如链、树)。

🖼️ 大图看明白

① 造价 2 ② 造价 3 ③ 造价 4 ④ 造价 5 6 7 8 1 2 3 4 5 起点:1 号岛 从 1 号岛出发,①→②→③→④ 依次接入,总造价 = 14 Prim 手推表(起点 1 号岛) 初始:树内 {1},dist:2=2,3=6,4=∞,5=8 轮 1:树外最便宜 = 2(造价 2)→ 接入,总 = 2 更新:从 2 报价 → dist[3] = min(6,3) = 3,dist[4] = min(∞,7) = 7 轮 2:树外最便宜 = 3(造价 3)→ 接入,总 = 5 更新:从 3 报价 → dist[4] = min(7,4) = 4 轮 3:树外最便宜 = 4(造价 4)→ 接入,总 = 9 更新:从 4 报价 → dist[5] = min(8,5) = 5 轮 4:树外最便宜 = 5(造价 5)→ 接入,总 = 14 树满 5 点 4 边 → 完成,总造价 = 14 和 Kruskal 选了同样的 4 座桥,总造价同样 14

🧩 子任务 1:Prim vs Kruskal——修路队 vs 种田队

🎯 小目标:分清两种 MST 算法的「视角」(按边挑 vs 从点长),会对照它们的工具(排序 + 同组表 vs dist 账本)。

同一道题(5 岛 7 桥),两种贪心视角。先把「两队」的打法摆在一起:

// 同一道题,两种打法(都是贪心,都是最小生成树):

// Kruskal(修路队):
//   把全部桥按造价排序,从便宜到贵逐座看:
//   两端「还没通」→ 修;两端「已通」(同组)→ 跳过(会绕圈)
//   修满 n − 1 座停。视角:站在「桥」上看。
//   工具:排序 + 同组表(判环)。

// Prim(种田队):
//   先占住自家岛(起点,如 1 号岛),树里只有 1 个点;
//   每轮在所有「树里 ↔ 树外」的跨界桥里修最便宜的一座,
//   把外面的岛接进树;接满 n − 1 座(全岛进树)停。
//   视角:站在「点」(领地)上看。
//   工具:dist 账本(树外每个岛接进树最便宜花多少)。

// 对照账(同一张 5 岛图):
//        Kruskal          Prim
// 起点   全体桥一起看     从 1 个岛出发
// 每次   挑 1 座桥        接 1 个新岛
// 工具   排序 + 同组表    dist 账本
// 本课   修 2,3,4,5 → 14 接 2,3,4,5 → 14(一样的答案)

// 两个都是「最小生成树」:具体选的桥可能不同
//(同价桥时),但总造价一定相同——下一课算「账」比快慢。
✅ 小检查:① 两种算法每步各自在问什么问题?② 什么情况下两种算法会选不同的桥?③ 为什么「选的桥可能不同,总造价一定相同」?
看看答案 ① Kruskal 问:「这座桥两端通没通?通了就跳过」;Prim 问:「哪个新岛能最便宜地接进我的树?」——一个盯桥,一个盯点。② 有同价桥时:两个算法都「合法」地选了其中一座,另一座被跳过/没报价,选出的树形状不同但花费一样。③ 最小生成树的总造价是唯一的(换桥论证保证贪心不亏;若有两个总造价不同的「最小」,便宜的才是真最小)——形状可以不同,花钱必须一样。
易错:把 Prim 的「每轮找最便宜的跨界桥」理解成「找全图最便宜的边」——Prim 只考虑一头在树里、一头在树外的桥。全图最便宜的边可能两头都在树外(轮不到它),也可能两头都在树里(已经用过了)。

🧩 子任务 2:Prim 手推——从 1 号岛扩张 4 轮

🎯 小目标:会维护 dist 账本(初始报价 → 每轮「选最小 + 用新岛再报价」),完整推出 5 岛 Prim 过程。

还是 5 岛 7 桥。Prim 每轮两件事:① 在树外找 dist 最小的点接进树;② 用新接入的点给其余树外点「报价」(有更便宜的路就更新 dist)。全程手推:

// 5 岛 7 桥:1-2:2  2-3:3  3-4:4  4-5:5  1-3:6  2-4:7  1-5:8
// 起点:1 号岛。dist[i] = 岛 i 接进树的最便宜桥造价

// 初始:树 = {1}。只有 1 在树里,用 1 的桥报价:
dist: 2 → 2(经 1-2)  3 → 6(经 1-3)  4 → ∞(1 和 4 没直桥)  5 → 8(经 1-5)

// 轮 1:树外 dist 最小 = 2(造价 2)→ 接 2 进树!总 = 2
// 树 = {1, 2}。用新岛 2 报价:2-3:3 < 6 → dist[3] = 3;2-4:7 < ∞ → dist[4] = 7
dist: 3 → 3(经 2-3)  4 → 7(经 2-4)  5 → 8

// 轮 2:树外 dist 最小 = 3(造价 3)→ 接 3 进树!总 = 5
// 树 = {1,2,3}。用新岛 3 报价:3-4:4 < 7 → dist[4] = 4
dist: 4 → 4(经 3-4)  5 → 8

// 轮 3:树外 dist 最小 = 4(造价 4)→ 接 4 进树!总 = 9
// 树 = {1,2,3,4}。用新岛 4 报价:4-5:5 < 8 → dist[5] = 5
dist: 5 → 5(经 4-5)

// 轮 4:树外 dist 最小 = 5(造价 5)→ 接 5 进树!总 = 14
// 树 = {1,2,3,4,5} → 全岛进树,完成!

// 结果:依次接入 1-2(2)、2-3(3)、3-4(4)、4-5(5),总造价 14
// 和 Kruskal 选了同样的 4 座桥、同样的总造价 ✓(本图桥价各不相同)
✅ 小检查:① 轮 3 开始时,dist[4] 为什么是 4 而不是 7?② 初始 dist[4] 为什么是 ∞?③ 桥 1-3(造价 6)从头到尾为什么没被选中?
看看答案 ① 初始 2 给 4 报价 7(2-4),轮 2 接入 3 后,3 给 4 报了更便宜的 4(3-4),dist[4] 从 7 更新为 4——dist 永远存「到目前为止最便宜的接法」。② 1 和 4 之间没有直接桥,初始谁也「够不到」4,记 ∞(一个比谁都大的数,保证之后任何真实报价都能把它替换掉)。③ 1-3 的造价是 6,但轮 2 选 3 时,dist[3] 已经被 2 更新成 3(2-3 更便宜)——3 号岛「走 2 号门进来」更划算,1-3 这座桥始终不是最便宜跨界桥,自然轮不到它。
易错:只「选最小」忘了「再报价」——接入新岛后不更新 dist,后面几轮就会按过期报价选人(比如一直用 7 接 4,而不是用更便宜的 4)。每轮两件事:选最小、再报价,一步都不能少。

🧩 子任务 3:实现「内/外」——dist 账本 + in 标记

🎯 小目标:会写 Prim 核心循环(找最小 + 再报价),理解 in[] 和 dist[] 的分工,会解释 INF 和「报价条件」的每个判断。

把手推过程翻译成代码。两张表分工:in[i] = 点 i 是否已在树里(内/外标记);dist[i] = 树外点 i 接进树的最便宜边权(账本)。邻接矩阵 g[i][j] 存桥造价(0 = 没桥):

// Prim 核心循环(5 点,邻接矩阵 g;INF = 999 表示「暂时无桥可接」)
// in[i]:点 i 在不在树里;dist[i]:树外点 i 接进树最便宜花多少

in[1] = true;  dist[1] = 0;              // 起点 1 在树里,接入成本 0
for (v = 1..5)
    if (g[1][v] > 0) dist[v] = g[1][v];  // 初始报价:只有 1 能报价
// 此时 dist = [0, 2, 6, INF, 8],和手推初始一致

for (轮 = 2..5) {                        // 还要接 4 个点
    // ① 树外找 dist 最小的点 u
    u = 0;
    for (i = 1..5)
        if (!in[i] && (u == 0 || dist[i] < dist[u]))
            u = i;
    in[u] = true;
    total += dist[u];                    // 接入成本记账
    // ② 新点 u 给其余树外点报价(更便宜才更新)
    for (v = 1..5)
        if (!in[v] && g[u][v] > 0 && g[u][v] < dist[v])
            dist[v] = g[u][v];
}
// 运行轨迹(u = 每轮接入的点):
// 轮 2:u = 2(dist 2)→ dist[3]=3, dist[4]=7
// 轮 3:u = 3(dist 3)→ dist[4]=4
// 轮 4:u = 4(dist 4)→ dist[5]=5
// 轮 5:u = 5(dist 5)→ 完成,total = 14 ✓ 和手推一致
✅ 小检查:① 第 3 轮(轮 3)执行完「报价」后,dist[4] 是多少?② 报价时为什么要检查 g[u][v] > 0?(不检查会出什么错?)③ 起点为什么设 dist[1] = 0?(不设行不行?)
看看答案 ① 轮 3 接入 3 后报价:3-4 造价 4 < 旧值 7 → dist[4] = 4(和手推一致)。② g[u][v] = 0 表示「没桥」;若不检查,0 比任何 dist 都小,会把 dist[v] 错误地改成 0——把「没桥」当成「免费桥」,后面会白选它。③ 起点接入成本就是 0(它一开始就在树里);in[1] = true 已保证 1 不会再被选,所以 dist[1] 设不设其实不影响结果,设 0 只是约定俗成、读代码时更清楚。
易错:「找最小」的循环忘了 !in[i] 条件——会把树里的点(dist 可能很小)也选进来,同一个岛接两次!选 u 的条件永远是:树外 + dist 最小。

🧩 子任务 4:复杂度对比账——稠密用 Prim,稀疏用 Kruskal

🎯 小目标:会算 Prim O(n²) 和 Kruskal O(m log m) 的实际步数,会用「稠密 → Prim,稀疏 → Kruskal」口诀选算法。

两种算法都快,但快的场景不一样。先把「账」算出来:

// Prim(邻接矩阵版)要多久?
// n − 1 轮,每轮:找最小扫 n 个点 + 报价扫 n 个点 → 每轮 O(n)
// 总计 O(n²)——只和点数有关,和边数 m 无关(矩阵里 0 也扫)

// Kruskal 要多久?
// 排序 m 条边 O(m log m) + 逐条扫 O(m) → O(m log m)
// 只和边数有关,和点数关系小

// 对账 1:稠密图(桥多)——n = 50,m = 1225(完全图,任意两岛都有桥)
// Prim:n² = 2500 步
// Kruskal:m log m ≈ 1225 × 10.3 ≈ 12600 步 → Prim 快约 5 倍

// 对账 2:稀疏图(桥少)——n = 1000,m = 1500(每岛平均才 3 座桥)
// Prim:n² = 1000000 步
// Kruskal:m log m ≈ 1500 × 10.6 ≈ 15900 步 → Kruskal 快约 60 倍

// 口诀:图「稠密」(m 接近 n²/2)→ Prim;
//       图「稀疏」(m 接近 n)→ Kruskal。
// (还有「优先队列版 Prim」,稀疏也快——那是更高级的课。)
✅ 小检查:① n = 100、m = 4950(完全图)的 MST,选哪个算法?(算两边步数)② 为什么 Prim 邻接矩阵版的复杂度 O(n²) 和 m 无关?③ n = 1000、m = 4000 的图,选哪个?为什么?
看看答案 ① Prim:n² = 10000;Kruskal:4950 × log₂4950 ≈ 4950 × 12.3 ≈ 60900 → 选 Prim(稠密,m = n(n−1)/2 顶格了)。② 矩阵版每轮「找最小 + 报价」都是把 1..n 的每个点扫一遍(没桥的格子 g=0 也要看),所以每轮固定 O(n)、共 n 轮 → O(n²),边多少不改变扫描量。③ m = 4000 远小于 n²/2 = 500000,属稀疏:Prim = 1000000 步,Kruskal ≈ 4000 × 12 ≈ 48000 步 → 选 Kruskal。
易错:背口诀不记账——「稠密/稀疏」不是感觉,是看 m 和 n² 的比例:m 接近 n²/2(完全图)= 稠密;m 只有几倍 n = 稀疏。拿不准就把两边的 O(n²) 和 O(m log m) 代数字算一遍,谁小用谁。

💻 完整代码:Prim(5 点邻接矩阵)→ 输出接入顺序 + 总权

// 输入:无(5 岛 7 桥写死,方便对答案)
// 输出:每轮接入的点 + 用的桥 + 总造价
// 答案:接 2(1-2,2) 3(2-3,3) 4(3-4,4) 5(4-5,5),总造价 = 14
#include <iostream>
using namespace std;

int g[6][6];     // 邻接矩阵:g[i][j] = 桥造价,0 = 没桥
int dist_[6];    // 树外点 i 接进树的最便宜边权
bool in[6];      // 点 i 是否已在树里

int main() {
    // 5 岛 7 桥:1-2:2  2-3:3  3-4:4  4-5:5  1-3:6  2-4:7  1-5:8
    g[1][2] = g[2][1] = 2;  g[2][3] = g[3][2] = 3;
    g[3][4] = g[4][3] = 4;  g[4][5] = g[5][4] = 5;
    g[1][3] = g[3][1] = 6;  g[2][4] = g[4][2] = 7;
    g[1][5] = g[5][1] = 8;

    const int INF = 999;
    for (int i = 1; i <= 5; i++) { dist_[i] = INF; in[i] = false; }
    in[1] = true;
    dist_[1] = 0;
    for (int v = 1; v <= 5; v++)            // 初始报价:只有 1 在树里
        if (g[1][v] > 0) dist_[v] = g[1][v];

    int total = 0;
    for (int round = 2; round <= 5; round++) {   // 还要接 4 个点
        // ① 树外找 dist 最小的点 u
        int u = 0;
        for (int i = 1; i <= 5; i++)
            if (!in[i] && (u == 0 || dist_[i] < dist_[u])) u = i;

        in[u] = true;
        total += dist_[u];

        // 找出 u 是「经哪座桥」接进来的(在树里、造价 = dist_[u] 的邻居)
        for (int p = 1; p <= 5; p++)
            if (in[p] && g[p][u] == dist_[u])
                cout << "接入 " << u
                     << "(经桥 " << p << "-" << u
                     << ",造价 " << dist_[u] << ")" << endl;

        // ② 新点 u 给树外点报价(更便宜才更新)
        for (int v = 1; v <= 5; v++)
            if (!in[v] && g[u][v] > 0 && g[u][v] < dist_[v])
                dist_[v] = g[u][v];
    }
    cout << "总造价 = " << total << endl;
    return 0;
}
// 运行结果:接入 2(1-2,2) 3(2-3,3) 4(3-4,4) 5(4-5,5) → 总造价 = 14
// 把 g[4][5] 改成 9:接入顺序变为 2、3、4、5(经 1-5,8)→ 总造价 = 17

✋ 你来试试

第 1 题:把 4-5 造价改成 9(最贵),Prim 从 1 出发会怎么接?总造价? 初始 dist:2=2, 3=6, 4=∞, 5=8。轮 1 接 2(2),报价 dist[3]=3、dist[4]=7;轮 2 接 3(3),报价 dist[4]=4;轮 3 接 4(4),报价 4-5:9 > 8 不更新,dist[5] 仍是 8(经 1-5);轮 4 接 5(8)。接入顺序 2、3、4、5,桥为 1-2、2-3、3-4、1-5,总造价 = 2+3+4+8 = 17——和 Kruskal 改价后的答案一致。
第 2 题:3 个岛,AB=1、BC=2、AC=3,Prim 从 A 出发,接入顺序和总造价? 初始 dist:B=1, C=3。轮 1 接 B(1),报价 B-C:2 < 3 → dist[C]=2;轮 2 接 C(2)。顺序 B、C,桥 AB、BC,总造价 = 3。注意:C 是「经 B」进来的(dist 被更新成 2),不是走最贵的 AC=3。
第 3 题:Prim 每轮「找最小」如果并列(两个树外点 dist 相同),程序会选谁?答案对吗? 程序选先扫到的(编号小的)——dist_[i] < dist_[u] 严格小于,后面的同值不替换。答案对:同价都是最便宜跨界桥,选谁都不亏(总造价相同,只是树形状可能不同)。
第 4 题(加餐):为什么 Prim 邻接矩阵版「每轮 O(n)」,就算图很稀疏也省不掉? 矩阵版「找最小」要把 1..n 每个点都扫一遍比较 dist(不知道哪个点有桥),「报价」也要扫全部 n 个点看 g[u][v]——没桥的格子(g=0)也要看过才能确认。所以每轮固定 2n 步、共 n 轮 = O(n²),边再少也省不了扫描量。稀疏图想省,就用 Kruskal 或「优先队列版 Prim」。
🎯 一句话记住:Prim = 种田队——守着一棵树往外长:每轮「树外选 dist 最小接入 + 用新点再报价」,接满 n−1 停;dist 账本 + in 标记;O(n²) 与 m 无关——稠密图用 Prim,稀疏图用 Kruskal(本课答案同样 14)。