0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1547,日期见页头。两边不一致时信原站。 ⚠ 这道题尤其要看清这一份 —— 它和 USACO 的原版输出格式不一样,见本页第 ⑤ 步。
题目描述
Bessie 计划调查 N(2 ≤ N ≤ 2000)个农场的干草情况,它从 1 号农场出发。农场之间总共有 M(1 ≤ M ≤ 10⁴)条双向道路,所有道路的总长度不超过 10⁹。有些农场之间存在着多条道路,所有的农场之间都是连通的。
Bessie 希望计算出该图中最小生成树中的最长边的长度。
输入格式
第一行两个整数 N, M。
接下来 M 行,每行三个用空格隔开的整数 Aᵢ, Bᵢ, Lᵢ,表示 Aᵢ, Bᵢ 之间有一条道路,长度为 Lᵢ。
输出格式
一个整数,表示最小生成树中的最长边的长度。
说明/提示
时限 1 秒,内存 125 MB。
输入输出样例
输入
3 3 1 2 23 2 3 1000 1 3 43
输出
43
三条路 23 / 1000 / 43。最小生成树选 1-2(23)和 1-3(43),
最长的那条是 43 —— 那条 1000 的路根本用不上。
1★ 关键的一步:答案就是 Kruskal 收下的最后一条边
Kruskal 是按权升序扫边的。它收下的那 n − 1 条边,权值也是升序的
⇒ 最小生成树里最长的那条,就是它收下的最后一条。
连 max 都不用取 —— 一个赋值就完了:
ans = t[0]; // 每收一条就覆盖一次,循环结束时它就是最后那条// P1547 [USACO05MAR] Out of Hay S —— ★ 这一版就能 AC//// ★ 关键的一步只有半行:Kruskal 是**按权升序**收边的// ⇒ 「最小生成树里最长的那条」就是**它收下的最后一条**。// 连 max 都不用取(取了也没错,见 p1547All.cpp 那个位置写错的反面教材)。//// ⚠ 题面明写着两句保证,各值一件事:// ①「所有的农场之间都是连通的」⇒ **不用判无解**(对照 [P3366](/sol/p3366/) 的 orz);// ②「所有道路的总长度不超过 10⁹」—— ★ 这句的主语是**总和**,// 于是连「权和」都撑不破 int,更别说单条边 ⇒ **不用 long long**。// ⚠ 另外题面明说「有些农场之间存在着多条道路」—— 重边。Kruskal 天然不怕,// 而换成邻接矩阵存图就得当心(见 p1547Mat.cpp)。
#include <bits/stdc++.h>using namespace std;
int fa[2005];int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); }
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n, m; cin >> n >> m; vector<array<int, 3>> e(m); for (auto& t : e) cin >> t[1] >> t[2] >> t[0]; // 读进来是 A B L,存成 (L, A, B)
sort(e.begin(), e.end()); for (int i = 1; i <= n; i++) fa[i] = i;
int ans = 0, cnt = 0; for (auto& t : e) { int x = find(t[1]), y = find(t[2]); if (x == y) continue; // ⚠ 跳过的边**不算数**,位置很要紧 fa[x] = y; ans = t[0]; // ★ 升序收边 ⇒ 最后收下的就是最长的 if (++cnt == n - 1) break; } cout << ans << '\n'; return 0;}点「运行 ▶」看结果
题面问的是「最小生成树里的最长边」,而这道题真正的用处是瓶颈生成树: 在所有生成树里,最长边最小的那棵。这两句话是不是一回事?
写一份和 Kruskal 一行不共享的参照物:枚举所有生成树,各算各的最长边,取最小 ——
| 300 组 | |
|---|---|
| 「最小生成树的最长边」≡「所有生成树里最长边的最小值」 | ★ 300 / 300 |
| 所有权和最小的生成树,最长边都一样 | ★ 300 / 300 |
★ 第二行是白送的推论也是必要的一步 —— 否则「最小生成树的最长边」这句话本身就没定义。 (背后是那条标准结论:所有最小生成树的边权多重集完全相同。)
⇒ 又一次「验算走一条和算法完全无关的路」。
2⚠ 最容易的一发 WA:打出了权和
// ✗ P1547:打出了最小生成树的**权和**//// 这道题挂在第 34 章的题单里,前面几道又全是「求权和」——// 于是写完 Kruskal 顺手 `cout << sum` 是最自然不过的一发 WA。// ★ 它是「每组都错」型(除非那棵树只有一条边),官方样例当场挡住(66 vs 43)。
#include <bits/stdc++.h>using namespace std;
int fa[2005];int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); }
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n, m; cin >> n >> m; vector<array<int, 3>> e(m); for (auto& t : e) cin >> t[1] >> t[2] >> t[0];
sort(e.begin(), e.end()); for (int i = 1; i <= n; i++) fa[i] = i;
long long sum = 0; int cnt = 0; for (auto& t : e) { int x = find(t[1]), y = find(t[2]); if (x == y) continue; fa[x] = y; sum += t[0]; // ✗ 题目问的是最长的那一条,不是和 if (++cnt == n - 1) break; } cout << sum << '\n'; return 0;}点「运行 ▶」看结果
这道题挂在第 34 章的题单里,前面几道又全是「求权和」—— 写完 Kruskal 顺手 cout << sum
是最自然不过的一发 WA。四个档全是 300 / 300,官方样例一测就死。
3★★★ 一个被实测打回来的草稿:max 写在 continue 前面,居然是对的
// ⚠⚠ P1547:取 max 那一行写在了 `continue` 前面 —— 而它**一次都没错**//// 草稿里把这一版当成错法收了进来,理由听着很顺:「被判环跳过的边也被算进 max 了,// 那不就成了**全图**最长的边?」——**四个档 1200 轮,被抓 0 次。**//// ★★★ 一行就能证明它为什么对:// 边是**升序**扫的,而 Kruskal 收够 `n − 1` 条就 `break`// ⇒ **被扫到的每一条边,都不比最后收下的那一条长。**// 于是 `max` 取到的就是最后收下的那条 —— 和正解一模一样。//// ⚠ 而这个「精确的 0」**配了自检才敢写**:把那句 `break` 去掉,它当场就错// (见 p1547NoBreak.cpp,档 0 抓 **203 / 300**)。// ⇒ 「[一个反例都没有](/sol/p1439/)」和「这段代码根本没在跑」输出一模一样。//// ⇒ ★★ 真正的结论不是「max 写哪儿都行」,是**「那句 break 在替它兜底」** ——// 两处细节各自都不起眼,凑在一起才让它是对的。
#include <bits/stdc++.h>using namespace std;
int fa[2005];int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); }
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n, m; cin >> n >> m; vector<array<int, 3>> e(m); for (auto& t : e) cin >> t[1] >> t[2] >> t[0];
sort(e.begin(), e.end()); for (int i = 1; i <= n; i++) fa[i] = i;
int ans = 0, cnt = 0; for (auto& t : e) { ans = max(ans, t[0]); // ✗ 跳过的边也被算进来了 int x = find(t[1]), y = find(t[2]); if (x == y) continue; fa[x] = y; if (++cnt == n - 1) break; } cout << ans << '\n'; return 0;}点「运行 ▶」看结果
草稿里把这一版当成错法收了进来,理由听着很顺:
「被判环跳过的边也被算进 max 了,那不就成了全图最长的边?」
四个档 1200 轮,被抓 0 次。 一行就能证明:
边是升序扫的,而 Kruskal 收够
n − 1条就break⇒ 被扫到的每一条边,都不比最后收下的那一条长。
于是 max 取到的恰好就是最后收下的那条 —— 和正解一模一样。
⇒ ★★ 真正的结论不是「max 写哪儿都行」,而是 那句 break 在替它兜底 ——
两处细节各自都不起眼,凑在一起才让它是对的。
「一个反例都没有」和「这段代码根本没在跑」输出一模一样。
所以这一页专门留了一份已知错的东西:同样的 max 位置,但没有那句 break。
// ✗ P1547(自检用):取 max 写在 `continue` 前面,**而且没有那句 break**//// ★★ 它的存在只有一个目的:证明「p1547All.cpp 被抓 0 次」不是因为对拍没在跑。// 两处细节各自都不起眼,凑在一起才让 p1547All 是对的 ——// 把 `break` 去掉,「跳过的边也算进 max」立刻变成一个实打实的 bug:// 它算的是**全图最长的那条边**。//// ⇒ [「报『精确的 0』之前,先拿一个已知错的东西验证这段代码是活的」](/sol/p1094/)// —— 这一份就是那个「已知错的东西」。
#include <bits/stdc++.h>using namespace std;
int fa[2005];int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); }
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n, m; cin >> n >> m; vector<array<int, 3>> e(m); for (auto& t : e) cin >> t[1] >> t[2] >> t[0];
sort(e.begin(), e.end()); for (int i = 1; i <= n; i++) fa[i] = i;
int ans = 0; for (auto& t : e) { ans = max(ans, t[0]); // ✗ 跳过的边也算进来 int x = find(t[1]), y = find(t[2]); if (x == y) continue; fa[x] = y; // ✗ 而且没有 break —— 一路扫到底 } cout << ans << '\n'; return 0;}点「运行 ▶」看结果
| 300 轮 | 档 0 | 档 1 重边 | ★ 档 2 输入本身是一棵树 | 档 3 大量并列 |
|---|---|---|---|---|
max 位置写错 + 保留 break |
★ 0 | ★ 0 | ★ 0 | ★ 0 |
max 位置写错 + 去掉 break |
268 | 298 | ★ 0 | 148 |
★★ 对拍代码是活的(268 / 298 / 148),⇒ 上面那一行的 0 是真的。 ★ 而档 2 那个 0 也是能证的:输入本身就是一棵树 ⇒ 一条边都不会被跳过,两版必然相同。 ⇒ 一张表里同时有「能证的 0」和「代码是活的」,这一档两件事一起干完了。
4★★ 重边:题面自己提醒了,而它只在一种存法上咬人
// ✗ P1547:邻接矩阵 + 朴素 Prim,重边直接赋值不取 min//// `n ≤ 2000` ⇒ 矩阵 2001 × 2001 × 4 ≈ **16 MB**(题面给 125 MB),**放得下**,// 而 `O(n²) = 4 × 10⁶` 也够快 ⇒ 朴素 Prim 在这道题上是一条**真能走通**的路。//// ★★ 可它比 Kruskal 多背了一个包袱:**题面明写着「有些农场之间存在着多条道路」。**// 矩阵一个格子只放得下一条边,顺手写 `g[a][b] = l` 的话,// **后读进来的那条会把先读进来的短边盖掉**。// ⇒ 这是 [B3647 那个坑](/sol/b3647/) 在 MST 上的复现,而这次**题面自己提醒过你了**。
#include <bits/stdc++.h>using namespace std;
const int INF = 0x3f3f3f3f;static int g[2005][2005];
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n, m; cin >> n >> m; memset(g, 0x3f, sizeof g); for (int i = 0; i < m; i++) { int a, b, l; cin >> a >> b >> l; g[a][b] = g[b][a] = l; // ✗ 没有 min —— 重边把短的盖掉了 }
vector<char> in(n + 1, 0); vector<int> best(n + 1, INF); best[1] = 0; int ans = 0; for (int it = 0; it < n; it++) { int u = -1; for (int i = 1; i <= n; i++) if (!in[i] && (u < 0 || best[i] < best[u])) u = i; in[u] = 1; ans = max(ans, best[u]); for (int i = 1; i <= n; i++) if (!in[i] && g[u][i] < best[i]) best[i] = g[u][i]; } cout << ans << '\n'; return 0;}点「运行 ▶」看结果
n ≤ 2000 ⇒ 矩阵 2001 × 2001 × 4 ≈ 15 MB(题面给 125 MB),
O(n²) = 4 × 10⁶ 也够快 ⇒ 朴素 Prim 在这道题上是一条真能走通的路。
可它比 Kruskal 多背一个包袱:矩阵一个格子只放得下一条边。
| 那句「有多条道路」 | Kruskal | 邻接矩阵 + Prim |
|---|---|---|
| 分量 | ★ 噪声(find 天然把重边判成环) |
★★ 命门(少半行 min 就错) |
| 档 1(重边档)· 300 轮 | |
|---|---|
| 输入里真的有重边 | 300 |
| 被盖掉的短边真的改变了答案 | 129 |
| ⇒ 「不取 min」被抓 | ★ 129 |
★ 触发条件 300、真被抓 129 ⇒ 两层,差 2.3 倍 —— 第一层写得太粗(有重边 ≠ 它影响答案)。 ⚠ 而顺手写的生成器在「互不相同的点对」里挑边,结构上造不出重边 ⇒ 另外三档全是精确的 0。
★★ 「这句约束重不重要,是『题目 × 你写的那一版』的属性」—— 这条本书已经量到第五、六次了。
// ★ P1547 的另一条路:朴素 Prim(矩阵存图,**重边取 min**)—— 它也能 AC//// 和上面那份 p1547Mat.cpp 只差 `min(...)` 那半行 ——// ★ 这一对是本页最干净的对照:**同一个写法,加不加那半行就是正解和错法。**// 矩阵 2001 × 2001 × 4 ≈ 16 MB,`O(n²) = 4 × 10⁶`,顶格本机见本页第 ⑥ 步那张表。
#include <bits/stdc++.h>using namespace std;
const int INF = 0x3f3f3f3f;static int g[2005][2005];
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n, m; cin >> n >> m; memset(g, 0x3f, sizeof g); for (int i = 0; i < m; i++) { int a, b, l; cin >> a >> b >> l; g[a][b] = g[b][a] = min(g[a][b], l); // ★ 就是这半行 }
vector<char> in(n + 1, 0); vector<int> best(n + 1, INF); best[1] = 0; int ans = 0; for (int it = 0; it < n; it++) { int u = -1; for (int i = 1; i <= n; i++) if (!in[i] && (u < 0 || best[i] < best[u])) u = i; in[u] = 1; ans = max(ans, best[u]); for (int i = 1; i <= n; i++) if (!in[i] && g[u][i] < best[i]) best[i] = g[u][i]; } cout << ans << '\n'; return 0;}点「运行 ▶」看结果
A 机 · WSL2 · 8 核 / 7 G · 2026-08-31 · 独占 · 顶格 n = 2000 / m = 10⁴(输入 146 756 字节)· 5 次取最小 |
|
|---|---|
| Kruskal | 4 毫秒 |
朴素 Prim(矩阵 15 MB,含 memset) |
19 毫秒 |
⇒ 两条路都过得轻松,选 Kruskal 的理由不是快,是「它不用为重边操心」。
5⚠ 和算法完全无关的一条:这道题有两份不同的题面
// ✗ P1547:按 USACO 原版的格式,输出了**两个**数//// ★ 这个坑不在算法里,在「你照着哪一份题面写」上:// USACO 原题要的是「最长边 + 有多少条边的长度等于它」,// 而本地存的这份洛谷题面(见页头「题目原文」)写得很清楚:// 输出格式是「**一个整数**」,样例输出也只有一个 `43`。// ⇒ 网上不少题解是照原版写的,**照抄会多打一个数**。// ⚠ 两边不一致时信原站 —— 这也正是每个解析页都要把题面存一份的理由。
#include <bits/stdc++.h>using namespace std;
int fa[2005];int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); }
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n, m; cin >> n >> m; vector<array<int, 3>> e(m); for (auto& t : e) cin >> t[1] >> t[2] >> t[0];
sort(e.begin(), e.end()); for (int i = 1; i <= n; i++) fa[i] = i;
int ans = 0, cnt = 0; for (auto& t : e) { int x = find(t[1]), y = find(t[2]); if (x == y) continue; fa[x] = y; ans = t[0]; if (++cnt == n - 1) break; }
int same = 0; for (auto& t : e) if (t[0] == ans) same++; cout << ans << ' ' << same << '\n'; // ✗ 这一行多出来的那个数 return 0;}点「运行 ▶」看结果
USACO 的原题要的是两个数(最长边 + 有多少条边的长度等于它), 而本地存的这份洛谷题面(见第 ⓪ 步)写得很清楚:
输出格式:一个整数,表示最小生成树中的最长边的长度。
样例输出也只有一个 43。⇒ 网上不少题解是照原版写的,照抄会多打一个数。
★ 这一条对拍逐字节比抓得到(四档全是 300 / 300),但它和算法一个字的关系都没有 ——
和 P3366 那个 Orz 是同一类:唯一的防线是把题面看完,并且看的是正确的那一份。
⚠ 而这也是每个解析页都要转录题面的理由:两边不一致时信原站,
但至少你手里有一份写着日期的存档,知道自己当时照的是哪一份。
6★ 题面那两句保证,各值一件事
| 题面保证的总长度 | ≤ 10⁹ |
int 上限 |
2 147 483 647 ⇒ 余量 ★ 2.1 倍 |
⇒ 于是连「所有道路的权和」都撑不破 int,单条边更不可能 ⇒ 不用 long long。
★ 这是「答案 ≥ 任何一个被用到的中间值」那条论证模式的反向用法:
那些页是从答案往上界推,这道题是题面直接把总和的上界给了你。
⚠ 而余量只有 2.1 倍 —— 要是题面写的是「不超过 10¹⁰」,结论就翻过来了。
造一档违反它的数据(点分两块)跑 300 组:
| Kruskal 版 | 朴素 Prim 版 | |
|---|---|---|
| 结论 | 照样给出「最小生成森林」的最长边 | ★ 打出 1061109567 |
| 两版分家 | ★ 300 / 300 |
那个 1061109567 就是 0x3f3f3f3f —— Prim 从 1 号点长,够不着的点 best 一直是 INF,
而 max 把它收了下来。
⇒ ★★ 这一页第二次量到「同一句约束,两个写法分量不同」(上一次是重边)—— 而这一次连方向都反了:重边那次是 Prim 更脆,连通性这次还是 Prim 更脆。 ⇒ 选 Kruskal 的理由到这儿凑齐了两条,都不是「快」。
7★ 对拍这一页
参照物是枚举所有生成树:找出权和最小的那棵,取它的最长边。
300 轮(n 随机 4~6) |
档 0 默认 | ★★ 档 1 重边 | ★ 档 2 输入是树 | ★ 档 3 大量并列 |
|---|---|---|---|---|
| Kruskal(正解) | ★ 0 | ★ 0 | ★ 0 | ★ 0 |
| 朴素 Prim(取 min) | ★ 0 | ★ 0 | ★ 0 | ★ 0 |
| 打出权和 | 300 | 300 | 300 | 300 |
max 位置写错(有 break) |
★ 0 | ★ 0 | ★ 0 | ★ 0 |
max 位置写错(无 break) |
268 | 298 | ★ 0 | 148 |
| 输出两个数 | 300 | 300 | 300 | 300 |
| 矩阵存图不取 min | ★ 0 | ★ 129 | ★ 0 | ★ 0 |
| 300 组 | 档 0 | ★ 档 3 大量并列 |
|---|---|---|
| Kruskal 和 Prim 选的边不同 | 7 | ★ 72 |
| 两版最长边不同 | ★ 0 | ★ 0 |
⇒ 和 P1546(6 / 39)、P2820(8 / 74)连成第三个点: 并列一多,两个都正确的写法就会分岔,而题目要的那个量从来没变过。 ★ 这一章三道题各量了一遍,结论一次没变 ⇒ 比题目要的那个数,别比边集。
| 错法 | 抓获率(最狠的那一档) | 官方样例挡住了吗 |
|---|---|---|
| 打出权和 | 300 / 300 | ★ 挡住了(打出 66) |
max 写错 + 无 break |
298 / 300 | ★ 挡住了(打出 1000) |
| 输出两个数 | 300 / 300 | ★ 挡住了(打出「43 1」) |
| 矩阵存图不取 min | 129 / 300 | ⚠ 放过了 —— 样例三条边、没有重边 |
⇒ 又一次:放过的那个不是因为它罕见(在它的档上 129 / 300 不算低),
是因为那三行 1 2 23 / 2 3 1000 / 1 3 43 结构上问不出这个问题(P1746 那条)。
8度量程序和生成器
9一页纸
| ★★ 关键的一步 | 升序收边 ⇒ 答案就是最后收下的那一条,一个赋值就完 |
| ★★★ 走一条无关的路验 | 「MST 的最长边」≡「所有生成树里最长边的最小值」300 / 300 |
| ★★★ 草稿被打回 | max 写在 continue 前面是对的 —— ★ 是那句 break 在兜底 |
| ⚠ 而那个 0 配了自检 | 去掉 break 当场 268 / 300 ⇒ 「代码是活的」和「能证的 0」同一张表 |
| ★★ 重边 | 题面明说了 —— 对 Kruskal 是噪声,对邻接矩阵 + Prim 是命门(300 触发 / 129 被抓) |
| ★★ 保证连通 | 同样两个写法分量不同:Prim 会把 0x3f3f3f3f 当答案打出来(300 / 300) |
| ⚠ 两份题面 | USACO 原版要两个数、洛谷这份要一个 ⇒ 照抄题解会多打一个数 |
| ★ 算术 | 「总长度 ≤ 10⁹」⇒ 连权和都撑不破 int(余量 2.1 倍);矩阵 15 MB / 125 MB |