0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1195,日期见页头。两边不一致时信原站。
题目背景
小杉坐在教室里,透过口袋一样的窗户看口袋一样的天空。
有很多云飘在那里,看起来很漂亮,小杉想摘下那样美的几朵云,做成棉花糖。
题目描述
给你云朵的个数 N,再给你 M 个关系,表示哪些云朵可以连在一起。
现在小杉要把所有云朵连成 K 个棉花糖,一个棉花糖最少要用掉一朵云,小杉想知道他怎么连,花费的代价最小。
输入格式
第一行有三个数 N, M, K。
接下来 M 行每行三个数 X, Y, L,表示 X 云和 Y 云可以通过 L 的代价连在一起。
输出格式
对每组数据输出一行,仅有一个整数,表示最小的代价。
如果怎么连都连不出 K 个棉花糖,请输出 No Answer。
说明/提示
对于 30% 的数据,1 ≤ N ≤ 100,1 ≤ M ≤ 10³;
对于 100% 的数据,1 ≤ N ≤ 10³,1 ≤ M ≤ 10⁴,1 ≤ K ≤ 10,1 ≤ X, Y ≤ N,0 ≤ L < 10⁴。
时限 1 秒,内存 512 MB。
输入输出样例
输入
3 1 2 1 2 1
输出
1
三朵云、一条可连的关系、要连成 2 个棉花糖。 从 3 个连通块降到 2 个只要合并 1 次 ⇒ 用掉那唯一一条边,代价 1。
1★ 关键的一步:连成 K 个,就是少合并 K − 1 次
一开始 n 朵云各自成块。每合并一次,连通块就少一个。
| 连成 1 个(普通最小生成树) | 合并 n − 1 次 |
| 连成 K 个 | ★ 合并 n − K 次 |
⇒ 本章第 6 步那份 Kruskal,把 n − 1 改成 n − K,一个字都不用再动。
★ 而无解的判据也跟着白送:合并不满 n − K 次,就说明原图的连通块本来就多于 K 个 ——
边不够,怎么连都下不来。
// P1195 口袋的天空 —— ★ 这一版就能 AC//// ★ 关键的一步只有一句话:**连成 K 个棉花糖 ⇒ 只要少合并 K − 1 次。**// 从 n 朵孤零零的云开始,每合并一次连通块就少一个 ⇒ 要从 n 降到 K,// 恰好合并 n − K 次。Kruskal 的循环**本来就在数这个**,把 `n − 1` 改成 `n − K` 就完了。//// 无解的判据也跟着白送:合并不满 n − K 次 ⇒ 原图的连通块本来就多于 K 个。// ⚠ 外加一个题面允许、而 `n − K` 那个减法藏不住的边界:**K 可能比 n 还大**// (题面 1 ≤ K ≤ 10 而 1 ≤ N,n = 3 / K = 10 完全合法)—— n 朵云最多分成 n 组。
#include <bits/stdc++.h>using namespace std;
int fa[1005];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, k; cin >> n >> m >> k; vector<array<int, 3>> e(m); for (auto& t : e) cin >> t[1] >> t[2] >> t[0]; // 读进来是 X Y L,存成 (L, X, Y)
if (k > n) { cout << "No Answer\n"; return 0; } // ⚠ n 朵云分不出 k 个非空的堆
sort(e.begin(), e.end()); for (int i = 1; i <= n; i++) fa[i] = i;
int sum = 0, cnt = 0; for (auto& t : e) { if (cnt == n - k) break; // ★ 够了就停 —— 剩下的边一条都不要 int x = find(t[1]), y = find(t[2]); if (x == y) continue; fa[x] = y; sum += t[0]; cnt++; }
if (cnt == n - k) cout << sum << '\n'; else cout << "No Answer\n"; // 合并不满 ⇒ 连通块本来就多于 k 个 return 0;}点「运行 ▶」看结果
无解 ⟺
K > N,或者原图的连通块数> K。
拿它去和「正解真的输出了 No Answer」逐组对照 —— 四个档共 1200 组,一个不差(1200 / 1200)。
⇒ 这就是本书反复用的那个动作:验算走一条和算法完全无关的路。 数连通块和 Kruskal 的循环没有半点关系,它却把那一支的判据完全钉死了。
2★★★ 「先求完整 MST 再删掉最大的 K − 1 条」—— 这个第一反应是对的
很多人的第一反应不是「少收几条」,而是「先把整棵最小生成树求出来,再从里面删掉最贵的 K − 1 条边」。 听起来像绕了一圈,而且很容易被当成错法枪毙掉。
// ★ P1195 的另一条路:先求完整的最小生成树,再**删掉最大的 K − 1 条边**//// 这是很多人的第一反应,而它**是对的** —— 和正解 300 组逐组相等(见本页第 ② 步)。// 一行就能证:Kruskal 是按权**升序**收边的,// 完整 MST 的边集就是它收下的那 n − 1 条;「前 n − K 条」正是其中**最小的 n − K 条**// ⇒ 「少收 K − 1 条」和「收完再删掉最大的 K − 1 条」是同一个集合。//// ⚠ 而它比正解**多了一处要小心的地方**:图不连通时压根没有「完整 MST」,// 所以判无解的那一句还是得靠数合并次数 —— **绕的这一圈没省掉任何东西。**
#include <bits/stdc++.h>using namespace std;
int fa[1005];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, k; cin >> n >> m >> k; vector<array<int, 3>> e(m); for (auto& t : e) cin >> t[1] >> t[2] >> t[0];
if (k > n) { cout << "No Answer\n"; return 0; }
sort(e.begin(), e.end()); for (int i = 1; i <= n; i++) fa[i] = i;
vector<int> used; // 最小生成森林收下的边,权值升序 for (auto& t : e) { int x = find(t[1]), y = find(t[2]); if (x == y) continue; fa[x] = y; used.push_back(t[0]); }
if ((int)used.size() < n - k) { cout << "No Answer\n"; return 0; }
long long sum = 0; for (int i = 0; i < n - k; i++) sum += used[i]; // ★ 丢掉最后(最大)的 k − 1 条 cout << sum << '\n'; return 0;}点「运行 ▶」看结果
Kruskal 是按权升序收边的。完整最小生成森林的边集,就是它收下的那一串;
而「前 n − K 条」正是这一串里最小的 n − K 条。
⇒ 「少收 K − 1 条」和「收完再删掉最大的 K − 1 条」,删的是同一批边。
实测印证(顶格 n = 1000 / m = 10⁴,k 随机 1~10,200 组):
| 两条路结论相同(含「有没有解」和权和) | ★ 200 / 200 |
⚠ 但结论不是「随便挑一条写」:绕这一圈没省掉任何东西,还多了一处要小心的地方 —— 图不连通时压根没有「完整 MST」,判无解那一句还是得靠数合并次数。 ⇒ ★ 这是「更优的写法也可能一分钱都不值」的邻居: 这里是「等价的写法也可能一分钱都不值」,而它多收了你一份心智负担。
3⚠ 差一:n − K 还是 n − K + 1
// ✗ P1195:合并次数差一 —— 停在 `n − k + 1` 次//// 「连成 k 个棉花糖要合并几次」这句话在纸上很容易写反:// 从 n 个块降到 k 个块要 **n − k** 次,而不是 n − k + 1(那会连成 k − 1 个)。// ★ 它是个「每组都错」型的 bug —— 官方样例当场挡住(多收一条边,答案变大)。// ⚠ 除非那一步没有边可收了(图刚好不够连),这时它反而和正解一样输出 No Answer。
#include <bits/stdc++.h>using namespace std;
int fa[1005];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, k; cin >> n >> m >> k; vector<array<int, 3>> e(m); for (auto& t : e) cin >> t[1] >> t[2] >> t[0];
if (k > n) { cout << "No Answer\n"; return 0; }
sort(e.begin(), e.end()); for (int i = 1; i <= n; i++) fa[i] = i;
int sum = 0, cnt = 0, need = n - k + 1; // ✗ 多合并了一次 for (auto& t : e) { if (cnt == need) break; int x = find(t[1]), y = find(t[2]); if (x == y) continue; fa[x] = y; sum += t[0]; cnt++; }
if (cnt == need) cout << sum << '\n'; else cout << "No Answer\n"; return 0;}点「运行 ▶」看结果
「从 n 个块降到 k 个块要合并几次」这句话在纸上很容易写反。
它是个「几乎每组都错」型的 bug —— 官方样例一测就死(样例里只有一条边,多要一条就没了)。
| 300 轮 | 档 0 默认 | 档 1 k 可能 > n |
档 2 边权可为 0 | ★ 档 3 边很少 |
|---|---|---|---|---|
| 多合并了一次 | 255 | 165 | 226 | ★ 97 |
★ 档 3 反而抓得最少,理由很干脆:那一档边本来就不够(m 只有 1~3),
两版一起输出 No Answer —— 又一次「一致有两种:都算对了,和都没算」。
4★★★ 边权可以是 0 —— 于是答案也可以是 0
题面的数据范围里写着 0 ≤ L < 10⁴。这半个字符(下界是 0 不是 1)值一整个错法:
// ✗ P1195:拿「答案还是 0」当「无解」的标志//// ★★ 这是「[答案有没有可能等于哨兵值](/sol/p1434/)」那条判据的又一个考场版 ——// 而这道题的题面**明写着** `0 ≤ L < 10⁴`:**边权可以是 0,于是答案真的可以是 0。**// ⚠ 而「答案恰好是 0」有**两条**路:① n == k(一条边都不用连)② 输入里有 0 权边。// 顺手写的生成器一律从 1 开始撒边权 ⇒ 只剩下第一条路,抓获率被压掉一大半。//// ⚠⚠ 这一份第一版**同时**错了两件事(顺手把「合并够没够」那一句也删了),// 于是量出来的抓获数其实是另一个 bug 的 —— 本章 wrongDij.cpp 顶上写着的那条规矩:// **错误版本也要干净,一份只错一件事**,否则说不清抓获率是谁的功劳。
#include <bits/stdc++.h>using namespace std;
int fa[1005];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, k; cin >> n >> m >> k; vector<array<int, 3>> e(m); for (auto& t : e) cin >> t[1] >> t[2] >> t[0];
if (k > n) { cout << "No Answer\n"; return 0; }
sort(e.begin(), e.end()); for (int i = 1; i <= n; i++) fa[i] = i;
int sum = 0, cnt = 0; for (auto& t : e) { if (cnt == n - k) break; int x = find(t[1]), y = find(t[2]); if (x == y) continue; fa[x] = y; sum += t[0]; cnt++; }
if (cnt != n - k) cout << "No Answer\n"; // ✓ 这一句是对的 else if (sum == 0) cout << "No Answer\n"; // ✗ 「一分钱都没花」≠「连不出来」 else cout << sum << '\n'; return 0;}点「运行 ▶」看结果
P1216 / P1434 那一对给过这条判据。这道题的答案真的可以是 0, 而且是两条不同的路:
| 300 组里「答案恰好是 0」 | 档 0(边权 1~20) | ★ 档 2(边权 0~3,题面允许) |
|---|---|---|
| 合计 | 18 | ★ 76 |
· 因为 n == k(一条边都不用连) |
18 | 18 |
| · 因为输入里有 0 权边 | ★ 0 | ★ 58 |
| ⇒ 「拿 ans == 0 当无解」被抓 | ★ 18 | ★ 76 |
★★ 触发条件 ≡ 抓获数,两档都一个不差。
⚠ 而顺手写 1 + rng() % 20 的生成器结构上只留下了第一条路 ——
抓获率被压掉 4.2 倍,而那不是「概率低」,是少了一整类输入。
第一版的 p1195Zero.cpp 顺手把「合并够没够」那一句也删掉了。
于是它量出来的 45 / 135 / 45 / 203 其实是隔壁那个 bug 的抓获数,
和「哨兵混淆」一点关系都没有。
⇒ 本章 wrongDij.cpp 顶上写着的那条规矩:
错误版本也要干净,一份只错一件事 —— 两个 bug 混在一份代码里,
量出来的抓获率说不清是谁的功劳。改干净之后,那一列从 45 掉到了 18。
5⚠ 另外两个:忘了 No Answer,和忘了 K 可能大于 N
// ✗ P1195:忘了「怎么连都连不出 K 个棉花糖」那一支//// 和 [P3366](/sol/p3366/) 那个「忘了判 n − 1 条」是同一个形状:// 它输出的不是垃圾,是**最小生成森林**的权和 —— 一个看着完全合理的数。// ⚠ 而这道题比 P3366 更容易漏:那边题面把 `orz` 说了两遍,// 这边 `No Answer` 只在输出格式里出现过一次。
#include <bits/stdc++.h>using namespace std;
int fa[1005];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, k; cin >> n >> m >> k; 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 sum = 0, cnt = 0; for (auto& t : e) { if (cnt == n - k) break; int x = find(t[1]), y = find(t[2]); if (x == y) continue; fa[x] = y; sum += t[0]; cnt++; } cout << sum << '\n'; // ✗ 从头到尾没检查够不够 return 0;}点「运行 ▶」看结果
和 P3366 那个「忘了判 n − 1 条」是同一个形状:
它输出的不是垃圾,是最小生成森林的权和 —— 一个看着完全合理的数。
⚠ 而这道题比 P3366 更容易漏:那边题面把 orz 说了两遍,这边 No Answer 只出现过一次。
| 300 轮 | 档 0 | 档 1 | 档 2 | ★ 档 3 边很少 |
|---|---|---|---|---|
正解真的输出 No Answer |
45 | 135 | 45 | 203 |
| ⇒ 「忘了这一支」被抓 | ★ 45 | ★ 135 | ★ 45 | ★ 203 |
★★ 四个档全部一个不差(能证:两版只在这一支上分家)。
// ✗ P1195:忘了 K 可能比 N 还大//// 题面写的是 `1 ≤ N ≤ 10³` 和 `1 ≤ K ≤ 10` —— **两条范围各说各的,中间没有任何关系**。// 于是 `n = 3, k = 10` 是一组完全合法的输入,而 n 朵云最多分成 n 组 ⇒ 答案是 No Answer。//// ★ 少了那一句之后 `n - k` 变成负数,循环第一次就 `cnt == n - k` 不成立、// 一条边都不收,最后落到「合并不满」那一支 —— **它居然也输出 No Answer**。// ⚠ 所以这个 bug 要现形,得**另有一条路**让 cnt 走到负数那个数……// 实测它其实**只在 `n == k` 附近才和正解分家**,见本页第 ⑤ 步那张表。
#include <bits/stdc++.h>using namespace std;
int fa[1005];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, k; cin >> n >> m >> k; 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 sum = 0, cnt = 0; for (auto& t : e) { if (cnt >= n - k) break; // ✗ 少了 k > n 的那一句 int x = find(t[1]), y = find(t[2]); if (x == y) continue; fa[x] = y; sum += t[0]; cnt++; }
if (cnt >= n - k) cout << sum << '\n'; // ✗ n − k 是负数时这里恒成立 else cout << "No Answer\n"; return 0;}点「运行 ▶」看结果
1 ≤ N ≤ 10³……1 ≤ K ≤ 10
两条约束之间没有任何联系 ⇒ n = 3, k = 10 是一组完全合法的输入,而三朵云分不出十个棉花糖。
⚠ 而顺手写的生成器几乎一定让 k 跟着 n 走(k = rng() % n + 1 这种),
于是这个 bug 在档 0 / 2 / 3 上都是精确的 0。
把 k 放开到 18、6:n 仍是 3
| 档 1 · 300 轮 | |
|---|---|
k > n 的组数 |
116 |
| ⇒ 「忘了这一句」被抓 | ★ 116 |
★ 又一个一个不差 —— ⇒ 这一页四个错法的触发条件,全都精确地等于它们的抓获数。
6★ 对拍这一页
参照物是枚举所有边的子集:挑一批边、数一数连出来几个连通块、是 K 个就记权和取最小 ——
它连「按权排序」这个念头都没有。
300 轮(n 随机 3~6) |
档 0 默认 | ★ 档 1 k 放开 |
★★ 档 2 边权可为 0 | ★ 档 3 边很少 |
|---|---|---|---|---|
| 正解 | ★ 0 | ★ 0 | ★ 0 | ★ 0 |
| 先求完整 MST 再删 | ★ 0 | ★ 0 | ★ 0 | ★ 0 |
| 多合并了一次 | 255 | 165 | 226 | 97 |
| 拿 ans == 0 当无解 | 18 | 46 | ★ 76 | 18 |
忘了 No Answer |
45 | 135 | 45 | ★ 203 |
忘了 k > n |
★ 0 | ★ 116 | ★ 0 | ★ 0 |
| 错法 | 触发条件 | 触发几组 | 真被抓 |
|---|---|---|---|
| 拿 ans == 0 当无解 | 答案恰好是 0(档 2) | 76 | ★ 76 |
忘了 No Answer |
正解输出 No Answer(档 3) | 203 | ★ 203 |
忘了 k > n |
k > n(档 1) |
116 | ★ 116 |
| 多合并了一次 | (两层,不等价) | —— | 255 |
⇒ 前三个全是能证的等价(两版只在那一支上分家),所以「一个不差」不是巧合。 ⚠ 而第四个不是 —— 它的触发条件是两层的,别把这条规律当定律: 同一件事在 P1803 上差 60 倍、在 B3637 上差 150 倍。
| 错法 | 抓获率(最狠的那一档) | 官方样例挡住了吗 |
|---|---|---|
| 多合并了一次 | 255 / 300 | ★ 挡住了(打出 No Answer) |
忘了 No Answer |
203 / 300 | ⚠ 放过了(样例有解) |
| 拿 ans == 0 当无解 | 76 / 300 | ⚠ 放过了(样例答案是 1) |
忘了 k > n |
116 / 300 | ⚠ 放过了(样例 k = 2 < n = 3) |
⇒ 又一次那条规律的主语问题:后三个在各自的档上都不算稀有,
样例放过它们不是因为它们偶发,是因为那一行 3 1 2 结构上问不出那三个问题。
7★ 两句一乘就完的算术,和一句噪声
答案上界 (n − 1) × maxL |
999 × 9999 = 9 989 001 |
int 上限 |
2 147 483 647 ⇒ 余量 ★ 214 倍 |
| 顶格输入 | n = 1000 / m = 10⁴ ⇒ 126 731 字节,端到端 5 毫秒 |
★ 顺带称一下自环(题面 1 ≤ X, Y ≤ N 没排除 X == Y):
| 四个档合计 1200 组 | |
|---|---|
| 输入里有自环 | 825 组 |
「特判 x == y 就跳过」和「不特判」结论不同 |
★ 0 组 |
★ 和 P3366 那次一样,这个 0 不是空壳(825 组是真有自环),
而且理由一行能证:自环两头 find 相同,Kruskal 的判环那一句已经把它挡掉了。
⇒ 前 两 轮那句提醒在 Kruskal 上一律是噪声 ——
⚠ 换成邻接矩阵存图就不是了。
8度量程序和生成器
9一页纸
| ★★ 关键的一步 | 连成 K 个 ⇒ 少合并 K − 1 次;把 n − 1 改成 n − K,一个字不用再动 |
| ★★★ 第一反应是对的 | 「先求完整 MST 再删最大的 K − 1 条」等价(顶格 200 / 200),⚠ 但一分钱不省 |
| ★ 无解的判据 | K > N 或连通块数 > K —— 拿它对 1200 组,一个不差 |
★★★ 0 ≤ L |
边权可以是 0 ⇒ 答案可以是 0 ⇒ 拿 ans == 0 当哨兵就错(18 → 76) |
| ★ 两行范围各说各的 | K ≤ 10 和 N ≥ 1 没有关系 ⇒ n = 3, k = 10 合法(抓 116) |
| ★★★ 四个一个不差 | 三个错法的触发条件 ≡ 抓获数,而且都是能证的等价 |
| ⚠⚠ 自己踩的 | 错误版本里混进了第二个 bug ⇒ 量出来的 45 是别人的功劳,改干净后是 18 |