🌉

最小生成树:5 个岛修最便宜的桥

5 个岛都要能互相走,桥一座一座修——每次修最便宜的,但不能修出「绕圈」的桥。修满 n−1 座就停,总花费最少:这就是最小生成树(MST)。

💡 打个比方

海上有 5 个岛(图里的「点」),有些岛对之间能修桥(「边」),每座桥造价不同(边的「权」)。目标是:让所有岛都能互相到达,花的钱最少。

先想清楚要修几座:5 个岛,一座一座连——1 座连 2 个岛、2 座连 3 个岛、4 座连 5 个岛。规律:n 个岛最少要 n − 1 座桥。少一座,有岛「孤零零」连不上;多一座,必然出现绕圈(A→B→C→A),圈上的某座桥就是白花钱——去掉它,岛还是全通的。

所以最优方案一定长这样:恰好 n − 1 座桥、全岛连通、不绕圈——这叫生成树(n 个点 n−1 条边的连通图,形似一棵树,没有环);在所有生成树里总造价最小的,就是最小生成树。

怎么找到它?Kruskal 算法(克拉斯卡尔,1956 年):把所有桥按造价从便宜到贵排序,然后一条一条看:这座桥连的两个岛如果还不通(不同组)就修;如果已经通了(同组,再修就绕圈)就跳过。修满 n − 1 座就停。就这么简单——便宜优先,绝不绕圈!

术语:图(点 + 边;边带数字叫「带权图」,数字叫权/造价);连通(任意两点能沿边走到);生成树(原图的一个子图:所有 n 个点都在、恰好 n−1 条边、连通、无环);最小生成树 MST(总权最小的生成树);环/圈(沿边走能回到起点的闭合路径,如 A→B→C→A);并查集(管「谁和谁同组」的表格,Kruskal 判环的工具,本课用白话版)。

🖼️ 大图看明白

2 3 4 5 6 ✗ 7 ✗ 8 ✗ 1 2 3 4 5 5 个岛修 4 座桥(5 − 1),总造价 = 14 Kruskal 手推表(按造价从便宜到贵) 排序结果:2, 3, 4, 5, 6, 7, 8 桥 1-2(造价 2)→ 不同组 ✓ 修,合并 桥 2-3(造价 3)→ 不同组 ✓ 修,合并 桥 3-4(造价 4)→ 不同组 ✓ 修,合并 桥 4-5(造价 5)→ 不同组 ✓ 修,合并 桥 1-3(造价 6)→ 已同组 ✗ 跳过(会绕圈) 桥 2-4(造价 7)→ 已同组 ✗ 跳过(会绕圈) 桥 1-5(造价 8)→ 已同组 ✗ 跳过(会绕圈) 修满 4 座(n − 1)→ 停!总造价 = 14 绿线 = 修的桥;灰虚线 = 跳过的桥

🧩 子任务 1:什么是生成树 / 最小生成树

🎯 小目标:记住生成树三条件(n 个点、n−1 条边、连通无环),会枚举 4 岛图的桥方案,找出最便宜的。

4 个岛 A、B、C、D,6 座候选桥及造价。生成树 = 恰好 4 − 1 = 3 座桥、全岛连通、不绕圈。把 3 座桥的组合挨个看:

// 4 岛候选桥:AB=2  BC=3  CD=4  DA=5  AC=6  BD=7
// 生成树 = 3 座桥 + 连通 + 无环(多 1 座必成环,少 1 座必断连)

// 挑几个组合算一算:
AB + BC + CD = 2 + 3 + 4 = 9    // A-B-C-D 一条链 ✓ 树,最便宜
AB + BC + DA = 2 + 3 + 5 = 10   // B 当「枢纽」✓ 树
AB + CD + DA = 2 + 4 + 5 = 11   // A 当「枢纽」✓ 树
AB + AC + BD = 2 + 6 + 7 = 15   // D-B-A-C ✓ 树(贵)
AB + BC + AC = 2 + 3 + 6 = 11   // ✗ A、B、C 绕了圈,D 还孤零零——不是树!

// 最便宜的树:AB + BC + CD = 9 → 最小生成树

// 用 Kruskal 复核(排序后逐条加):
// 排序:AB(2) BC(3) CD(4) DA(5) AC(6) BD(7)
AB(2) → 不同组 ✓ 修    // {A,B} {C} {D}
BC(3) → 不同组 ✓ 修    // {A,B,C} {D}
CD(4) → 不同组 ✓ 修    // {A,B,C,D} → 3 座 = 4−1,完成
总造价 = 2 + 3 + 4 = 9  // 和枚举答案一致 ✓
✅ 小检查:① 为什么 AB + BC + AC 不是生成树?(说出两个理由)② 6 个岛最少要几座桥才能全连通?③ 「最便宜的 3 座桥」一定组成生成树吗?(用 AB+BC+AC 想一想)
看看答案 ① 理由一:A、B、C 之间 AB、BC、AC 三座桥绕成圈(A→B→C→A);理由二:岛 D 一座桥都没修,没连通。两个条件都不满足。② 6 − 1 = 5 座(n 个岛 n−1 座:少一座有孤岛,多一座必绕圈)。③ 本例里最便宜 3 座 AB+BC+CD 恰好成链是树;但一般不一定——如果最便宜的 3 座恰好围成一个三角形,第四个岛就被晾在外面了。Kruskal 的「同组就跳过」正是防这个。
易错:把「n − 1 条边」当成「随便哪 n−1 条」——边数对了还可能不连通(两个小圈各玩各的)或有环(圈 + 孤岛)。生成树 = 边数对 且 连通 且 无环,三个条件缺一不可。

🧩 子任务 2:Kruskal 手推——5 岛 7 桥完整过程

🎯 小目标:完整走一遍 Kruskal 三步(排序 → 逐条「不同组就修、同组就跳过」→ 修满 n−1 停),会边推边维护「分组」。

5 个岛、7 座候选桥。Kruskal 三步:① 按造价排序;② 逐条处理(不同组 → 修 + 合并,同组 → 跳过);③ 修满 5 − 1 = 4 座就停。全程手推:

// 候选桥:1-2:2  2-3:3  3-4:4  4-5:5  1-3:6  2-4:7  1-5:8

// 第 1 步:按造价排序(便宜 → 贵)
1-2(2)  2-3(3)  3-4(4)  4-5(5)  1-3(6)  2-4(7)  1-5(8)

// 第 2 步:逐条处理(「分组」= 现在哪些岛已经互相通了)
1-2(2): 1、2 不同组 → 修!合并 → {1,2} {3} {4} {5}
2-3(3): 2、3 不同组 → 修!合并 → {1,2,3} {4} {5}
3-4(4): 3、4 不同组 → 修!合并 → {1,2,3,4} {5}
4-5(5): 4、5 不同组 → 修!合并 → {1,2,3,4,5}
        ── 已修 4 座 = 5 − 1,全岛同组 ──
// 第 3 步:修满 n − 1 → 停!后面的桥都不用看了
1-3(6): (若继续看)1、3 已同组 → 跳过(1→2→3 通了,再加 1-3 成三角圈)
2-4(7): 已同组 → 跳过
1-5(8): 已同组 → 跳过

// 结果:修 1-2、2-3、3-4、4-5,总造价 = 2 + 3 + 4 + 5 = 14
// 自检:5 个点 4 条边、连通、无环 ✓ 是生成树;
// 总造价 14 是所有生成树里最小的(正确性见子任务 4)。
✅ 小检查:① 为什么修满 4 座就可以停,后面的 6、7、8 不用再看了?② 如果继续看,桥 1-3 为什么「会绕圈」?③ 若 4-5 造价变成 9(最贵),结果会怎样?(重新排序再推一遍)
看看答案 ① 4 座 = n − 1,全岛已在同一组——再修任何桥,两端必同组,必成环(白花钱),所以停。② 1 和 3 已经通过 1-2、2-3 连通(同组);再修 1-3,路径 1→2→3→1 闭合成三角形,圈上的桥至少有一座是多余的。③ 新排序:1-2(2)、2-3(3)、3-4(4)、1-3(6)、2-4(7)、1-5(8)、4-5(9)。推:1-2✓、2-3✓、3-4✓ → {1,2,3,4} {5};1-3(6) 同组 ✗;2-4(7) 同组 ✗;1-5(8) 不同组 → ✓ 修!修满 4 座停。结果:1-2、2-3、3-4、1-5,总造价 = 2+3+4+8 = 17。4-5 变贵了,Kruskal 自动「改道」用 1-5。
易错:手推时不更新分组——修了 1-2 却忘了把 1、2 记进同一组,后面判断 1-3 时就会误判「不同组」。每修一座桥,立刻把两个岛的组合并,纸上画「{1,2,3} {4} {5}」这样的分组最稳。

🧩 子任务 3:「成环」怎么判——同组表

🎯 小目标:会建「同组表」group[](谁和谁已经通了),掌握「同组就跳过、不同组就修 + 合并」的判环口诀(并查集思想的白话版)。

怎么知道「修这座桥会不会绕圈」?看两个岛现在通不通:已经通了(同组)→ 再修必绕圈;还没通(不同组)→ 放心修。用一张同组表记:group[i] = 岛 i 所在组的编号(用组长当编号):

// 初始:每个岛自己一组 group = [1, 2, 3, 4, 5]

// 1-2(2):group[1]=1 ≠ group[2]=2 → 不同组 → 修!
// 合并(2 的组并入 1 的组,统一记组长 1):
group = [1, 1, 3, 4, 5]

// 2-3(3):group[2]=1 ≠ group[3]=3 → 不同组 → 修!
// 合并:group[3] 改成 1
group = [1, 1, 1, 4, 5]

// 3-4(4):group[3]=1 ≠ group[4]=4 → 不同组 → 修!
group = [1, 1, 1, 1, 5]

// 4-5(5):group[4]=1 ≠ group[5]=5 → 不同组 → 修!
group = [1, 1, 1, 1, 1]   // 全岛同组 → 修满 4 座,完成

// 1-3(6):group[1]=1 = group[3]=1 → 同组 → 跳过(会绕圈)
// 2-4(7)、1-5(8) 同理:同组 → 跳过

// 口诀(并查集 Union-Find 的白话版):
// Find:查 group[u] 和 group[v] 等不等
//   相等 → 同组 → 跳过
//   不等 → 修桥 → Union:把两组合并(全改成同一组长)
✅ 小检查:① 修完 1-2 和 2-3 后,group 是什么?能判断 1 和 3 同组吗?② 为什么「同组」就能推出「再修一定绕圈」?③ 合并时为什么把 group[3] 改成 1,而不是把 group[1]、group[2] 改成 3?
看看答案 ① group = [1, 1, 1, 4, 5];group[1] = group[3] = 1 → 1 和 3 同组(虽然它们之间没有直接修桥,但 1-2-3 通了)。② 同组 = 已经有一条路能从 u 走到 v;再修 u-v 这座桥,这条旧路 + 新桥就闭合成圈(u→旧路→v→新桥→u),圈上必有冗余。③ 合并方向任意都行,只要「同一组同一个编号」不破坏就行。选「并入已有大组的组长」只是习惯;编程时常用「小编号并入大编号」或「按组大小并」来让表保持短平快。
易错:判完「不同组」就修桥,忘了合并——表不更新,下一座桥的判断就全错了。修桥和合并是同一个动作的两半:修!→ 立刻改表!

🧩 子任务 4:为什么 Kruskal 一定最优?要算多久?

🎯 小目标:能用「换桥论证」说清「最便宜且不成环的桥一定该修」,会算复杂度(排序 O(m log m) 是大头)。

Kruskal 是「贪心」:每次只挑眼前最便宜且不成环的桥。敢贪心,是因为有个换桥论证(用大白话说):

// 换桥论证(为什么「最便宜且不成环的桥 e 一定该修」):
// e 是队列里最便宜、且在 Kruskal 表里「两端不同组」的桥:
//   它的两端 u、v 在表里分属两个组(还没通)。
// 第 1 步:假设某个最优方案 S 里没有 e。把 e 加进 S:
//          S 是树(n 点全通、无环),加一条边必多出恰好一个圈
//          (S 里连接 u、v 的旧路 + e)。
// 第 2 步:这个圈上必有一座桥 f「跨越两个组的边界」:
//          u 在 A 组、v 在 B 组,旧路必然要从 A 组走到 B 组,
//          「跨界」的那座桥就是 f。
// 第 3 步:f 的造价 ≥ e 的造价。反证:若 f 比 e 便宜,
//          排序时 f 排在 e 前面,Kruskal 早处理 f 时
//          f 两端也是不同组(组只会越并越大)→ 早就修了 f,
//          现在 f 两端已同组,f 就不可能「跨界」——矛盾!
// 第 4 步:从 S 里拆掉 f、换上 e → 边数没变、不断连、圈消掉,
//          还是生成树,总造价没变贵(e ≤ f)。
// 结论:任何最优方案都能「换」成含 e 的一样优的方案
//       → 每步贪心都不亏 → Kruskal 的结果一定是最优的 ✓

// 复杂度(m 条边):
// ① 排序:O(m log m) ← 最慢的一步(大头)
// ② 逐条看:每条边查 1 次组 + 至多合并 1 次 → O(m)
// 总计 ≈ O(m log m)(m = 200 时,排序约 200×7.6 ≈ 1500 步)

// 对比暴力法(枚举所有生成树):
// 本课 7 条边选 4 条:C(7,4) = 35 种组合,还要逐个查连通——
// m = 200 时组合数是天文数字;Kruskal 只需排一次序 + 扫一遍。
✅ 小检查:① 为什么「n 个点、n−1 条边、无环」就一定是连通的?(反证:假设不连通)② 100 个岛、300 座候选桥,Kruskal 的主要开销在哪一步?③ 换桥论证里,为什么圈上一定存在「造价 ≥ e」的桥 f 可以换?
看看答案 ① 假设不连通:n 个点至少分成 2 块,每块内部无环 → 每块边数 ≤ 块点数 − 1,总边数 ≤ (n − 1) − 1 = n − 2,与「有 n−1 条边」矛盾 → 必连通。② 排序 300 条边:O(300 log 300) ≈ 300 × 8.2 ≈ 2500 次比较(扫边 O(m) 可忽略)——排序是大头。③ 圈上必有一座「跨界」桥 f(u 在 A 组、v 在 B 组,旧路必然要从 A 走到 B)。若 f 比 e 便宜,排序时 f 排在 e 前面,Kruskal 处理 f 时 f 两端也是不同组(组只会越并越大)→ 早就修了 f,现在 f 两端已同组,就不可能跨界——矛盾。所以 f 的造价 ≥ e,换 e 进来不亏。
易错:把 Kruskal 和「每个点连最近的点」(单点贪心)混为一谈——Kruskal 是对「边」排序,每次看的是「这条边两端通不通」,而不是「这个点缺不缺边」。点视角的贪心是 Prim 算法(下一课)。

💻 完整代码:5 岛 7 桥 Kruskal(同组表判环 + 排序 + 修满 n−1 停)

// 输入:无(5 岛 7 桥直接写死在程序里,方便对答案)
// 输出:每座「修的桥 / 跳过的桥」+ 总造价
// 答案:修 1-2、2-3、3-4、4-5,总造价 = 14
#include <iostream>
#include <algorithm>
using namespace std;

struct Edge { int u, v, w; };   // 一座桥:两端 u、v,造价 w
Edge e[7];
int grp[5];                     // 同组表:grp[i] = 岛 i+1 的组号

bool byW(const Edge &a, const Edge &b) { return a.w < b.w; }
int findGroup(int x) { return grp[x - 1]; }
void unionGroup(int a, int b) {
    int ga = grp[a - 1], gb = grp[b - 1];
    for (int i = 0; i < 5; i++)
        if (grp[i] == gb) grp[i] = ga;   // b 的组并入 a 的组
}

int main() {
    // 候选桥:1-2:2  2-3:3  3-4:4  4-5:5  1-3:6  2-4:7  1-5:8
    e[0] = {1, 2, 2};  e[1] = {2, 3, 3};  e[2] = {3, 4, 4};
    e[3] = {4, 5, 5};  e[4] = {1, 3, 6};  e[5] = {2, 4, 7};
    e[6] = {1, 5, 8};

    for (int i = 0; i < 5; i++) grp[i] = i + 1;   // 初始:各岛自己一组

    sort(e, e + 7, byW);          // 第 1 步:按造价排序(便宜 → 贵)

    int cnt = 0, total = 0;
    for (int i = 0; i < 7 && cnt < 4; i++) {      // 修满 5 − 1 = 4 座就停
        if (findGroup(e[i].u) != findGroup(e[i].v)) {
            cout << "修桥 " << e[i].u << "-" << e[i].v
                 << "(造价 " << e[i].w << ")" << endl;
            unionGroup(e[i].u, e[i].v);          // 修桥 → 立刻合并
            cnt++;
            total += e[i].w;
        } else {
            cout << "跳过 " << e[i].u << "-" << e[i].v
                 << "(已同组,会绕圈)" << endl;
        }
    }
    cout << "总造价 = " << total << endl;
    return 0;
}
// 运行结果:修 1-2(2) 2-3(3) 3-4(4) 4-5(5) → 总造价 = 14
// 把 e[3] 改成 {4,5,9} 再跑:修 1-2、2-3、3-4、1-5 → 总造价 = 17

✋ 你来试试

第 1 题:把 4-5 造价改成 9(最贵),程序会修哪几座桥?总造价? 新排序:1-2(2)、2-3(3)、3-4(4)、1-3(6)、2-4(7)、1-5(8)、4-5(9)。推:1-2✓、2-3✓、3-4✓ → {1,2,3,4} {5};1-3(6) 同组 ✗;2-4(7) 同组 ✗;1-5(8) 不同组 ✓ → 修满 4 座停。修 1-2、2-3、3-4、1-5,总造价 = 2+3+4+8 = 17。最贵的 4-5 被「改道」绕开了。
第 2 题:3 个岛,桥 AB=1、BC=2、AC=3,最小生成树总造价? 排序 AB(1)、BC(2)、AC(3):AB✓ 修 {A,B}{C};BC✓ 修 {A,B,C},修满 3−1=2 座停。总造价 = 1+2 = 3。AC(3) 不用看(看了也是同组跳过——A、B、C 已全通)。
第 3 题:两座桥造价相同(如 1-2:2 和 2-3:2),排序时谁在前会影响答案吗? 不影响总造价。例:3 岛 AB=2、BC=2、AC=2,无论 AB、BC 谁先排,都会修这两座、跳过 AC,总造价 = 4。同价的桥选哪座可能不同,但总造价一样(生成树的形状可能不一样,花钱一样)——程序用 sort 不用操心这个。
第 4 题(加餐):程序用「cnt < 4」停,也能改成「grp 全变同一个数」停吗?为什么? 能,两者等价:每修一座桥就合并一次,修满 n−1 座 ⟺ n 个岛全在同一组。n−1 计数更简单(一个 int 搞定),「全同组」判断要扫一遍表。竞赛里两个条件选一个写就行,写了两个也不会错(同时满足)。
🎯 一句话记住:Kruskal 三步——① 边从便宜到贵排序;② 不同组 → 修 + 合并,同组 → 跳过(会绕圈);③ 修满 n−1 座停。同组表判环;排序 O(m log m) 是大头;5 岛 7 桥 → 修 2+3+4+5,总造价 14。