🧰

图论工具箱:什么活用什么家伙

BFS、DFS、Kruskal、Prim、Dijkstra、Floyd 六个家伙都学过了。今天不学新算法,只练一件事:看题,选工具——别拿锤子拧螺丝,也别拿螺丝刀钉钉子。

💡 打个比方

家里有个工具箱,装锤子、螺丝刀、扳手、剪刀。干活的人不看说明书就拿一把乱砸——活是干完了,家具也砸了。算法也一样:图论的「工具箱」里有六个家伙,各自擅长一种活:

🔗 连通性(BFS / DFS):「这两点通不通?在不在同一块?」——不数距离,只标「到没到过」。🌉 连通全网最便宜(Kruskal / Prim,MST):「修几条路把所有村连起来、花的钱最少?」——选 n−1 条边。📍 单源最短路(Dijkstra):「从一个点到所有点,各走多短?」——盖章 + 松弛。🗺️ 全源最短路(Floyd):「任意两点都要查?」——一张 n×n 大表。

四件活,四件工具,选错工具不是算不出,而是算得慢或者干脆算错:用 BFS 数「跳数」去答「带权最短路」——跳数最少 ≠ 路程最短(上上节课 1→3 的例子:2 跳的 5 比 1 跳的 6 便宜);用 Dijkstra 去答「连通全网最便宜」——它只关心「从一个点出发的路」,根本不挑边。

选工具的三步:① 看问什么(连通?全网?最短路?);② 看规模(n 多大、边多少——稀疏还是稠密);③ 看权(全相等 → BFS;非负 → Dijkstra 可用;可为负 → Floyd)。今天用「4 个村修路 + 配送」和「5 岛图三问」练三遍,把「看题 → 选家伙」练成条件反射。

术语:图论工具箱(连通性 / MST / 单源最短路 / 全源最短路四类工具);稀疏图(边很少,m ≈ n,适合 Kruskal);稠密图(边很多,m ≈ n²,适合 Prim);工具选择(看「问什么 + 规模 + 权」三件事,而不是「哪个算法熟用哪个」)。

🖼️ 大图看明白

图论工具箱 连通性 · BFS/DFS 两点在不在一块? 标到过没,向外淹 连通全网 · MST 全网最便宜修法 稀疏 Kruskal 稠密 Prim 单源最短路 · Dijkstra 一个点到所有点 盖章 + 松弛,非负权 全源最短路 · Floyd 任意两点一张表 n 小或可负权(无负环) 工单 → 派工具 工单:两村通不通? → BFS / DFS 工单:修路连全网最便宜? → Kruskal / Prim 工单:总部配送最短? → Dijkstra 工单:任意两点都要查 → Floyd 同一张图,问题不同,答案不同! MST 总造价 ≠ 最短路总长,别把答案抄串

🧩 子任务 1:工具速查——先问「问什么」

🎯 小目标:看到题目先分清「问什么」,再按「规模 + 权」选工具。
// ── 图论工具箱速查表 ──
// 问什么                          工具              关键动作
// 「两点通不通 / 分几块」        BFS 或 DFS       标「到过没」
// 「全网最便宜连起来(n−1 条边)」  Kruskal 或 Prim   选边拼 MST
//   边少(稀疏) → Kruskal(排序选边)
//   边多(稠密) → Prim(树一点一点长)
// 「一个点到所有点最短路」        Dijkstra          盖章 + 松弛
// 「任意两点最短路(全表)」        Floyd             k 从 1 走到 n

// ── 选工具三步 ──
// ① 问什么?连通 / 全网 / 最短路(一步定大类)
// ② 规模?n 多大、边多少(稀疏选 Kruskal,稠密选 Prim;
//          全源 n ≤ 几百用 Floyd,n 上千跑 n 遍 Dijkstra)
// ③ 权?全相等 → BFS 数跳数就行;非负 → Dijkstra 可用;
//        可为负(无负环) → 只有 Floyd 能上
✅ 小检查:① 「判断 A、B 两点通不通」用谁?② 「100 个村、120 条候选路,修路连全网最便宜」用谁?③ 「n = 200,任意两点最短路都要」用谁?
看看答案 ① BFS/DFS——只问「通不通」,不用距离:从 A 出发淹一遍,看 B 有没有被标到。② Kruskal——「全网最便宜」是 MST 题;120 条边对 100 个点很稀疏(m ≈ n),排序选边最省。③ Floyd——要全表 + n = 200 → 200³ = 8,000,000 步,眨眼完成;(Dijkstra 跑 200 遍也是 O(n³) 同阶,但换起点麻烦)。
易错:「拿锤子拧螺丝」两大经典——用 BFS 数跳数答带权最短路(跳数最少 ≠ 路程最短:1→3 走 2 跳造价 5,比 1 跳造价 6 便宜);用 Dijkstra 答「全网最便宜」(它只算「从一个点出发的路」,从不挑边拼网)。

🧩 子任务 2:工单一——4 个村修路,成本最小

🎯 小目标:读题选对工具(Kruskal),手推修路方案,算出最小成本。
// 4 个村(A B C D)的候选公路造价:
//   A-B:3   B-C:2   C-D:4   A-C:6   B-D:5   A-D:9
// 工单:花最少的钱,让 4 个村路路相通(村数 n = 4)
// 选工具:「全网最便宜」→ MST;边少(6 条) → Kruskal(从最便宜开始选)

// 手推(把边按造价从小到大排队):
//   排队:B-C:2 → A-B:3 → C-D:4 → B-D:5 → A-C:6 → A-D:9
//   选 B-C:2  → 两端没连通,修!(A | B C | D)
//   选 A-B:3  → 两端没连通,修!(A B C | D)
//   选 C-D:4  → 两端没连通,修!(A B C D 全连通)→ 收工!
//   B-D:5 / A-C:6 / A-D:9 → 两端已连通,全跳过(再修就成环、白花钱)
// 方案:B-C + A-B + C-D,总成本 = 2 + 3 + 4 = 9,修 3 = n−1 条
✅ 小检查:① 为什么修完 C-D:4 就能收工?② 如果 A-D 造价是 1(超便宜),方案怎么变?总成本多少?
看看答案 ① 4 个村全进了同一块(连通),而且已经修满 n−1 = 3 条——再修任何一条都会绕成圈(比如 B-D 把 B-C-D 围成环),圈上必有一条是多余的。② 重新排队:A-D:1 → B-C:2 → A-B:3 → ……选 A-D:1(A D | B | C)→ 选 B-C:2(A D | B C)→ 选 A-B:3(全连通)→ 收工。方案 A-D + B-C + A-B,总成本 = 1+2+3 = 6——最便宜的路哪怕看起来「孤零零」,只要不绕圈就必须入选。
易错:① 「n 个村修 n 条路」——全网最便宜永远是 n−1 条,多一条必成环。② 「只修最便宜的一条就完事」——B-C:2 最便宜,但光修它连 B、C 两村,A、D 还断着;目标是「全连通 + 总成本最小」,不是「挑最便宜的边」。

🧩 子任务 3:工单二——总部 A 配送,各村最短多少

🎯 小目标:同一张路网换个问法 → 选 Dijkstra,手推总部 A 到每村的最短配送距离。
// 还是上一步那 4 个村的路网:A-B:3  B-C:2  C-D:4  A-C:6  B-D:5  A-D:9
// 工单:总部设在 A,送到每个村的最短路程各是多少?
// 选工具:「一个点到所有点最短路」→ Dijkstra(盖章 + 松弛)

// 手推(起点 A):
//   账本初始(A 的直达路):B = 3,C = 6,D = 9
//   盖章 B(3):松弛邻居 → C: min(6, 3+2) = 5 ✓ 更新!
//                          → D: min(9, 3+5) = 8 ✓ 更新!
//   盖章 C(5):松弛 D: min(8, 5+4) = 8 打平,不变
//   盖章 D(8):收工
//   最终:A→B = 3,A→C = 5(经 B),A→D = 8(走 A→B→D)
// 注意:A-D 直达造价 9 被「绕路」A→B→D = 8 淘汰了——带权图里绕远可以更短!

// 延伸:村委会要是老问「任意两村之间最短多少」(全源查询)?
//   村只有 4 个(n 小)→ 跑一遍 Floyd,把 4×4 全表存下,随问随答:
//   A-B=3  A-C=5  A-D=8  B-C=2  B-D=5  C-D=4
//   (4³ = 64 步,眨眼跑完;以后换「B 到 D」这种问法也不用重跑)
✅ 小检查:① A→C 为什么是 5 不是 6?② A-D 直达 9 为什么出局?③ 只有 4 个村,跑一遍 Floyd 值不值?
看看答案 ① A→B→C = 3+2 = 5 < 6——经 B 中转比直达便宜。② A→B→D = 3+5 = 8 < 9——直达桥 9 比绕路还贵,盖章 D 时账本上已经是 8。③ 值:4³ = 64 步瞬间跑完,换来一张「任意两村」全表——以后 A→D、B→D、C→A……随问随答;要是只问「A 到各村」,Dijkstra 一遍就够,Floyd 是「备全表」的保险。
易错:配送距离用 BFS 数跳数——A→D 直达 1 跳(造价 9),A→B→D 是 2 跳(造价 8):权不相等时「跳数少」反而「路程长」。带权必用 Dijkstra,只有所有边权都 = 1 时 BFS 的跳数才等于路程。

🧩 子任务 4:综合——5 岛 7 桥,一图三问

🎯 小目标:同一张图连问三件事,各选各的工具,三个答案别抄串。
// 老熟人了:5 岛 7 桥(1-2:2  2-3:3  3-4:4  4-5:5  1-3:6  2-4:7  1-5:8)
// 同一张图,三张工单:

// Q1「全网最便宜连起来」→ MST(Kruskal):
//   边排队:2 → 3 → 4 → 5 → 6 → 7 → 8
//   选 1-2(2)、2-3(3)、3-4(4)、4-5(5) → 5 岛全连通,收工
//   答案:总造价 = 2+3+4+5 = 14(4 = n−1 座桥)

// Q2「1 到 4 最短路」→ Dijkstra(起点 1):
//   盖章 2(2):dist[3]=5、dist[4]=9
//   盖章 3(5):4 打平(5+4=9)不变;盖章 5(8);盖章 4(9) 收工
//   答案:1→4 = 9(1→2→4 和 1→2→3→4 两条一样短)

// Q3「2 到 5 最短路」→ Dijkstra(换起点 2,重跑一遍!):
//   账本初始:1=2、3=3、4=7(2 的直达桥;5 没有直达)
//   盖章 1(2):5 经 1:2+8 = 10 → dist[5]=10
//   盖章 3(3):4 打平(3+4=7)不变;盖章 4(7):5 经 4:7+5 = 12 > 10 不变
//   盖章 5(10) 收工
//   答案:2→5 = 10(走 2→1→5,比经 4 的 12 便宜)

// 三问三答:14 / 9 / 10 —— 工具不同、起点不同,数字各归各位!
✅ 小检查:① Q1 用 Prim 算,答案会和 Kruskal 不一样吗?② Q2 的 1→4 有几条一样短的路?③ Q3 的 2→5 经过哪个岛中转?
看看答案 ① 一样的,都是 14——MST 的总造价唯一(具体选哪 4 座桥可能有多种选法,但总造价都是 14);Kruskal 和 Prim 只是「挑边的姿势」不同,目的地相同。② 两条:1→2→4 = 2+7 = 9 和 1→2→3→4 = 2+3+4 = 9,正好打平(Dijkstra 只记距离,不记「有两条」)。③ 经 1 号岛:2→1→5 = 2+8 = 10——起点换人后「中转站」也会换(从 1 出发去 4 经 2/3,从 2 出发去 5 经 1)。
易错:三个答案抄串位——14 是「桥的总造价」(MST 问的),9 和 10 是「路的总长」(最短路问的),数字长得像、单位其实回答的是不同的工单。交卷前自问一遍:「这个数字是哪张工单的答案?」

💻 完整代码:一张图跑 Kruskal + Dijkstra,两个答案对照

// 输入:无(5 岛 7 桥写死,方便对答案)
// 工具 1(Kruskal):全网最便宜 → 输出选中的桥 + 总造价(答案 14)
// 工具 2(Dijkstra):1 号岛到各岛最短路 → 输出账本(答案 0 2 5 9 8)
#include <iostream>
using namespace std;

int g[6][6];      // 邻接矩阵:g[i][j] = 桥造价,0 = 没桥
int fa[6];        // Kruskal 的并查集:fa[i] = i 所在「块」的组长
int dist_[6];     // Dijkstra 账本:1 到各岛的最短路
bool done_[6];    // Dijkstra:岛是否已盖章

int find(int x) {                    // 找 x 的组长(顺路把路压平)
    while (fa[x] != x) { fa[x] = fa[fa[x]]; x = fa[x]; }
    return x;
}
void unite(int a, int b) { fa[find(a)] = find(b); }  // 两块并成一块

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;
    for (int i = 1; i <= 5; i++) fa[i] = i;

    // ── 工具 1:Kruskal(造价从低到高试,两端不同块就修)──
    //(正式程序先 sort 边;这里造价只有 2..8,直接按造价从小到大试)
    int total = 0, used = 0;
    for (int w = 2; w <= 8; w++)
        for (int u = 1; u <= 5; u++)
            for (int v = u + 1; v <= 5; v++)
                if (g[u][v] == w && find(u) != find(v)) {
                    unite(u, v);
                    total += w; used++;
                    cout << "Kruskal 修 " << u << "-" << v
                         << "(造价 " << w << ")" << endl;
                }
    cout << "MST 总造价 = " << total << "(修了 " << used << " 座桥)" << endl;

    // ── 工具 2:Dijkstra(1 号岛出发,盖章 + 松弛)──
    const int INF = 999999;
    for (int i = 1; i <= 5; i++) { dist_[i] = INF; done_[i] = false; }
    dist_[1] = 0;
    for (int v = 1; v <= 5; v++) if (g[1][v] > 0) dist_[v] = g[1][v];
    for (int round = 1; round <= 5; round++) {
        int u = 0;
        for (int i = 1; i <= 5; i++)
            if (!done_[i] && (u == 0 || dist_[i] < dist_[u])) u = i;
        done_[u] = true;
        for (int v = 1; v <= 5; v++)
            if (g[u][v] > 0 && !done_[v] && dist_[v] > dist_[u] + g[u][v])
                dist_[v] = dist_[u] + g[u][v];
    }
    for (int i = 1; i <= 5; i++)
        cout << "Dijkstra: 1 到 " << i << " = " << dist_[i] << endl;
    return 0;
}
// 运行结果:
// Kruskal 修 1-2(造价 2)
// Kruskal 修 2-3(造价 3)
// Kruskal 修 3-4(造价 4)
// Kruskal 修 4-5(造价 5)
// MST 总造价 = 14(修了 4 座桥)
// Dijkstra: 1 到 1 = 0
// Dijkstra: 1 到 2 = 2
// Dijkstra: 1 到 3 = 5
// Dijkstra: 1 到 4 = 9
// Dijkstra: 1 到 5 = 8
// 同图两工具:14 是「修桥总造价」,0 2 5 9 8 是「1 出发的路长」——别抄串!

✋ 你来试试

第 1 题:4 村修路(子任务 2 那张图)里,把 A-B 造价改成 4,最小修路成本是多少?修哪三条? 重新排队:B-C:2 → C-D:4 → A-B:4 → B-D:5 → A-C:6 → A-D:9。选 B-C:2(A | B C | D)→ 选 C-D:4(A | B C D)→ 选 A-B:4(全连通)→ 收工。总成本 = 2+4+4 = 10,方案 B-C + C-D + A-B。原来 A-B:3 时是 9,涨价 1 总成本也涨 1(它仍是必须修的三条之一)。
第 2 题:5 岛图,3 号岛到 5 号岛的最短路是多少?走哪? Dijkstra 起点换 3:账本初始 3→4=4、3→2=3(3 的直达桥);盖章 2(3):1 经 2:3+2=5 → dist[1]=5;盖章 4(4):5 经 4:4+5=9 → dist[5]=9;盖章 1(5):5 经 1:5+8=13 > 9 不变;盖章 5(9) 收工。答案 9,走 3→4→5——3 号岛的「出口」就是 4 号岛,直达 5 的路(经 1 的 13)贵多了。
第 3 题:三张工单各派什么工具?① 判断整张图是否连通;② 首都到所有城市最短路(n = 500,权为正);③ 任意两城市最短路(n = 20,权可为负、无负环)。 ① BFS/DFS——只问通不通,从任意点淹一遍,看能不能标到所有点。② Dijkstra——只要单源,O(n²) = 250,000 步眨眼完成(Floyd 要 500³ = 1.25 亿步,杀鸡用牛刀)。③ Floyd——权可为负,Dijkstra 怕负权只有它能上;n = 20 才 20³ = 8,000 步,毫无压力。
第 4 题(加餐):5 岛图,Dijkstra 起点换成 5,5→2 最短路是多少?经谁? 账本初始(5 的直达桥):4=5、1=8。盖章 4(5):2 经 4:5+7=12 → dist[2]=12;3 经 4:5+4=9 → dist[3]=9。盖章 1(8):2 经 1:8+2=10 < 12 → dist[2]=10;3 经 1:8+6=14 > 9 不变。盖章 2(10)、3(9) 收工。答案 10,走 5→1→2(经 4 的 12 更贵)——起点从 1 换到 5,「谁当中转站」完全重排。
🎯 一句话记住:图论工具箱 = 什么活用什么家伙——先看「问什么」:连通 → BFS/DFS;全网最便宜 → Kruskal(稀疏)/ Prim(稠密);单源最短路 → Dijkstra(非负权);全源最短路 → Floyd(n 小或可负权);再看规模与权。同图三问三答案:MST 总造价 14、1→4 = 9、2→5 = 10——工具不同,答案各归各位,别抄串。