题单 · 习题解析

洛谷 P1195 口袋的天空

★★ 关键一步一句话:**连成 K 个就是少合并 K−1 次** ⇒ 把 `n−1` 改成 `n−K`,Kruskal 一个字不用再动;★★★ 「先求完整 MST 再删掉最大的 K−1 条」这个第一反应**是对的**(顶格 200 / 200,一行能证),⚠ 但绕这圈一分钱不省、还多一处要小心;★ 无解 ⟺ `K > N` 或**连通块数 > K**(1200 组一个不差);★★★ 题面 **`0 ≤ L`** ⇒ 答案真能是 0 ⇒ 拿 `ans == 0` 当哨兵就错(顺手生成器 18、放开 0 权边 **76**,两条触发路径分得清清楚楚);★ `K ≤ 10` 和 `N ≥ 1` **两行范围没有关系** ⇒ `n=3, k=10` 合法(抓 116);★★★ **四个错法里三个「触发条件 ≡ 抓获数」,全是能证的等价**;⚠⚠ 自己踩的:错误版本混进第二个 bug ⇒ 量出来的 45 是别人的功劳,改干净后是 18

原题:洛谷 P1195出自 第 34 章 最小生成树:Kruskal 与 Prim 的题单题面本地存档:2026-08-31
⚠ 先自己写一遍,再往下看

这一页不是标准答案,是一条阶梯:把大多数人真实会写出的第一版摆出来, 看它错在哪、慢在哪,再一步一步改。所以它对「已经自己动手撞过一次墙」的人最有用。
还没写过的话,先去写 —— 写出来的哪怕是错的,也比直接看这一页收获大。

0题目原文(本地存了一份)

为什么要在这儿抄一份题面

原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。

转录自洛谷 P1195,日期见页头。两边不一致时信原站。

题目背景

小杉坐在教室里,透过口袋一样的窗户看口袋一样的天空。

有很多云飘在那里,看起来很漂亮,小杉想摘下那样美的几朵云,做成棉花糖。

题目描述

给你云朵的个数 N,再给你 M 个关系,表示哪些云朵可以连在一起。

现在小杉要把所有云朵连成 K 个棉花糖,一个棉花糖最少要用掉一朵云,小杉想知道他怎么连,花费的代价最小。

输入格式

第一行有三个数 N, M, K

接下来 M 行每行三个数 X, Y, L,表示 X 云和 Y 云可以通过 L 的代价连在一起。

输出格式

对每组数据输出一行,仅有一个整数,表示最小的代价。

如果怎么连都连不出 K 个棉花糖,请输出 No Answer

说明/提示

对于 30% 的数据,1 ≤ N ≤ 1001 ≤ M ≤ 10³

对于 100% 的数据,1 ≤ N ≤ 10³1 ≤ M ≤ 10⁴1 ≤ K ≤ 101 ≤ X, Y ≤ N0 ≤ L < 10⁴

时限 1 秒,内存 512 MB。

输入输出样例

输入

3 1 2
1 2 1

输出

1

三朵云、一条可连的关系、要连成 2 个棉花糖。 从 3 个连通块降到 2 个只要合并 1 次 ⇒ 用掉那唯一一条边,代价 1

1★ 关键的一步:连成 K 个,就是少合并 K − 1 次

★★ Kruskal 的循环本来就在数这件事

一开始 n 朵云各自成块。每合并一次,连通块就少一个。

连成 1 个(普通最小生成树) 合并 n − 1
连成 K ★ 合并 n − K

本章第 6 步那份 Kruskal,把 n − 1 改成 n − K一个字都不用再动

★ 而无解的判据也跟着白送:合并不满 n − K 次,就说明原图的连通块本来就多于 K 个 —— 边不够,怎么连都下不来。

p1195.cpp★ 这一版就能 AC(顶格本机 5 毫秒)
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★ 「无解」的充要条件能写成一句话,而且验过了

无解 ⟺ K > N,或者原图的连通块数 > K

拿它去和「正解真的输出了 No Answer」逐组对照 —— 四个档共 1200 组,一个不差(1200 / 1200)

⇒ 这就是本书反复用的那个动作验算走一条和算法完全无关的路。 数连通块和 Kruskal 的循环没有半点关系,它却把那一支的判据完全钉死了。

2★★★ 「先求完整 MST 再删掉最大的 K − 1 条」—— 这个第一反应是对的

很多人的第一反应不是「少收几条」,而是「先把整棵最小生成树求出来,再从里面删掉最贵的 K − 1 条边」。 听起来像绕了一圈,而且很容易被当成错法枪毙掉。

p1195Full.cpp★ 另一条路 —— 它也能 AC(和正解 200 / 200 逐组相等)
// ★ 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★★ 它不只是「碰巧也对」—— 一行就能证明两者是同一个集合

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

p1195Off.cpp✗ 多合并了一次(样例打出 No Answer,当场挡住)
// ✗ 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

「从 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)值一整个错法:

p1195Zero.cpp✗ 拿「答案还是 0」当「无解」的标志(样例照过)
// ✗ 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★★ 「答案有没有可能等于哨兵值」是一句不用跑程序的判据 —— 而这次触发条件有两条路

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

p1195NoAns.cpp✗ 忘了「怎么连都连不出」那一支(样例照过)
// ✗ 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

P3366 那个「忘了判 n − 1 条」是同一个形状: 它输出的不是垃圾,是最小生成森林的权和 —— 一个看着完全合理的数。 ⚠ 而这道题比 P3366 更容易漏:那边题面把 orz 说了两遍,这边 No Answer 只出现过一次。

300 轮 档 0 档 1 档 2 ★ 档 3 边很少
正解真的输出 No Answer 45 135 45 203
⇒ 「忘了这一支」被抓 45 135 45 203

★★ 四个档全部一个不差(能证:两版只在这一支上分家)。

p1195Kbig.cpp✗ 忘了 K 可能比 N 还大(样例照过)
// ✗ 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★ 题面那两行范围各说各的 —— 中间一个字的关系都没有

1 ≤ N ≤ 10³ …… 1 ≤ K ≤ 10

两条约束之间没有任何联系n = 3, k = 10 是一组完全合法的输入,而三朵云分不出十个棉花糖。

⚠ 而顺手写的生成器几乎一定让 k 跟着 n 走(k = rng() % n + 1 这种), 于是这个 bug 在档 0 / 2 / 3 上都是精确的 0。 把 k 放开到 18、n 仍是 36:

档 1 · 300 轮
k > n 的组数 116
⇒ 「忘了这一句」被抓 116

★ 又一个一个不差 —— ⇒ 这一页四个错法的触发条件,全都精确地等于它们的抓获数。

6★ 对拍这一页

参照物是枚举所有边的子集:挑一批边、数一数连出来几个连通块、是 K 个就记权和取最小 —— 它连「按权排序」这个念头都没有。

p1195Brute.cpp参照物:枚举边子集数连通块(300 轮不一致 0 轮)
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★ 两句一乘就完的算术,和一句噪声

★ 要不要 long long:不用,余量 214 倍
答案上界 (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度量程序和生成器

p1195Count.cpp度量程序(本页所有数字都出自它)
p1195Gen.cpp(五个档位)数据生成器

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 ≤ 10N ≥ 1 没有关系n = 3, k = 10 合法(抓 116)
★★★ 四个一个不差 三个错法的触发条件 ≡ 抓获数,而且都是能证的等价
⚠⚠ 自己踩的 错误版本里混进了第二个 bug ⇒ 量出来的 45 是别人的功劳,改干净后是 18