题单 · 习题解析

洛谷 P3366 【模板】最小生成树

★★★ 模板题真正的关卡是最后半句「不连通输出 `orz`」—— 而「先造一棵树保证连通」的顺手生成器让它变成**精确的 0**(换成随机撒边 208、保证不连通 300);★★ **208 ≡ 图真的不连通的组数,一个不差而且能证**;⚠ 「`orz` 打成 `Orz`」抓获数和它**逐格相同** ⇒ 对拍别 strip;★★★ 「排序反了」**精确地在解最大生成树**(300 / 300 对上独立写的 Prim)⇒ 而档 2 那个 0 **是假的**(300 轮正解也输出 `orz`,在验零);★★★ 「没有路径压缩」答案永远对 ⇒ 顶格**随机** 2187 万步(50 ms)、**卡链档 19.6 亿步(2842 ms)**,差 89.7 倍;★★ 选型减掉 19 ms 的读入才看得清(Kruskal 11 vs 堆 Prim 30);⚠ 草稿被打回:朴素 Prim 的矩阵 95 MB **没爆** 128 MB

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

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

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

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

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

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

题目描述

如题,给出一个无向图,求出最小生成树,如果该图不连通,则输出 orz

输入格式

第一行包含两个整数 N, M,表示该图共有 N 个结点和 M 条无向边。

接下来 M 行每行包含三个整数 Xᵢ, Yᵢ, Zᵢ,表示有一条长度为 Zᵢ 的无向边连接结点 Xᵢ, Yᵢ

输出格式

如果该图连通,则输出一个整数表示最小生成树的各边的长度之和。如果该图不连通则输出 orz

说明/提示

数据规模:

对于 20% 的数据,N ≤ 5M ≤ 20

对于 40% 的数据,N ≤ 50M ≤ 2500

对于 70% 的数据,N ≤ 500M ≤ 10⁴

对于 100% 的数据:1 ≤ N ≤ 50001 ≤ M ≤ 2 × 10⁵1 ≤ Zᵢ ≤ 10⁴1 ≤ Xᵢ, Yᵢ ≤ N

时限 1 秒,内存 128 MB。

输入输出样例

输入

4 5
1 2 2
1 3 2
1 4 3
2 3 4
3 4 3

输出

7

样例解释(原题的图,本地存了一份):

P3366 样例解释:四个点五条边,最小生成树选中 2 + 2 + 3

所以最小生成树的总边权为 2 + 2 + 3 = 7

1第一版:把本章第 6 步的 Kruskal 照搬过来

这道题是模板题,本章第 6 步那份 kruskal.cpp 几乎可以原样交上去 —— 排序、能连就连、并查集判环,三件事一件不少。

p3366NoCheck.cpp✗ 第一版:照搬 Kruskal(官方样例打出 7,一个字不错)
// ✗ P3366 的第一版:忘了「选够 n − 1 条没有」
//
// 它在**连通**的图上一个字都不错 —— 官方样例照过,顺手写的对拍也照过。
// 图一旦不连通,它输出的是「最小生成**森林**」的权和:一个看着完全合理的正整数。
// ⇒ 这就是本页第 ③ 步那件事:**错法不输出垃圾,它输出一个像答案的东西。**
#include <bits/stdc++.h>
using namespace std;
int fa[5005];
int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); }
struct E { int u, v, w; };
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
vector<E> e(m);
for (auto& t : e) cin >> t.u >> t.v >> t.w;
sort(e.begin(), e.end(), [](const E& a, const E& b) { return a.w < b.w; });
for (int i = 1; i <= n; i++) fa[i] = i;
int sum = 0;
for (const auto& t : e) {
int x = find(t.u), y = find(t.v);
if (x == y) continue;
fa[x] = y;
sum += t.w;
}
cout << sum << '\n'; // ✗ 从头到尾没数过收了几条
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

它在官方样例上就是标准答案。 而它是错的 —— 错在题面最后那半句话上。

★★★ 这道模板题真正的关卡,是「如果该图不连通,则输出 orz」

题面把这半句话说了两遍(描述里一遍、输出格式里一遍)。而上面那一版从头到尾没数过自己收了几条边

图一旦不连通,Kruskal 的循环照样跑完,它给出的是最小生成森林的权和 —— 一个正的、看着完全合理的整数。

错法不输出垃圾,它输出一个像答案的东西。 这就是它能骗到人的全部原因。

⇒ 补的是一句话:树长成了必然恰好 n − 1 条边,数一数就行。

p3366.cpp★ 这一版就能 AC(Kruskal,顶格本机 30 毫秒)
// P3366【模板】最小生成树 —— ★ 这一版就能 AC
//
// Kruskal:把边按权排序,能连就连;数够 n − 1 条才算连通,否则输出 orz。
//
// ★ 这道题真正的考点不是「怎么求 MST」,是最后那一句:**图可能不连通**。
// 而顺手写的生成器(先造一棵树保证连通、再撒边)**一条不连通的图都造不出来**
// ⇒ 那一档对拍是精确的 0,见 p3366Gen.cpp 的档位 1。
//
// 答案上界:(n − 1) × Z ≤ 4999 × 10⁴ = 49 990 000 < 2³¹ − 1(余量 43 倍)⇒ **不用 long long**。
#include <bits/stdc++.h>
using namespace std;
int fa[5005];
int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); } // 路径压缩
struct E { int u, v, w; };
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
vector<E> e(m);
for (auto& t : e) cin >> t.u >> t.v >> t.w;
sort(e.begin(), e.end(), [](const E& a, const E& b) { return a.w < b.w; });
for (int i = 1; i <= n; i++) fa[i] = i;
int sum = 0, cnt = 0;
for (const auto& t : e) {
int x = find(t.u), y = find(t.v);
if (x == y) continue; // 同一棵树里再连就成环了
fa[x] = y;
sum += t.w;
if (++cnt == n - 1) break; // 够 n − 1 条,树已经长成
}
if (cnt == n - 1) cout << sum << '\n';
else cout << "orz\n"; // ⚠ 全小写。题面里那三个字母是 `orz`
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

2★★★ 而顺手写的生成器,一组不连通的图都造不出来

★★★ 「先造一棵树保证连通,再撒几条边」—— 这一行让上面那个 bug 变成精确的 0

写生成器时最自然的一招,就是先拉一棵树把所有点串起来,再随机撒几条边。 这一招在前面三十几页里用过无数次,因为它省事、而且大多数图论题都要求连通。

可这道题的考点恰恰就是不连通。

300 轮 ★ 档 0:先造树(顺手写的样子) ★ 档 1:照题面随机撒 m 条边 ★★ 档 2:保证不连通
「忘了判 n − 1 条」被抓 精确的 0 208 300

抓不到它的不是轮数,是档位 —— 档 0 加到一万轮仍然是 0,因为那一档的图永远连通

★★ 208 ≡ 208:触发条件和抓获数一个不差,而这一次是能证的等价

度量程序独立数了一遍「档 1 那 300 组里,图真的不连通有几组」:

档 1 里图真的不连通 208 / 300
「忘了判 n − 1 条」被抓 208 / 300

★ 一个不差,而且这次不用量也能证:图连通时它和正解一字不差; 图不连通时正解输出 orz、它输出一个数字 ⇒ 两件事互为充要条件

⇒ 「触发条件 ≡ 抓获数」这条本书量到过很多次(P2240 239 ≡ 239、 P1094 191 ≡ 191、P1077 三档全等 …), ★ 而能证的等价上一次是 P2865。 ⚠ 别把它当定律 —— 同一件事在 P1803 上差了 60 倍、在 B3637 上差 150 倍

3⚠ 和算法完全无关的那一条:orz 是三个小写字母

p3366Orz.cpp✗ 算法一个字没错,只是打了 Orz
// ✗ P3366:算法一个字没错,只是把 orz 写成了 Orz
//
// ★ 它是这一页唯一一个「和算法完全无关」的错法,也是唯一一个**对拍逐字节比才抓得到**的。
// ⚠ 而它和上一版(忘了判连通)在**同一个档位**才现形 —— 图连通的时候两个都是精确的 0。
#include <bits/stdc++.h>
using namespace std;
int fa[5005];
int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); }
struct E { int u, v, w; };
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
vector<E> e(m);
for (auto& t : e) cin >> t.u >> t.v >> t.w;
sort(e.begin(), e.end(), [](const E& a, const E& b) { return a.w < b.w; });
for (int i = 1; i <= n; i++) fa[i] = i;
int sum = 0, cnt = 0;
for (const auto& t : e) {
int x = find(t.u), y = find(t.v);
if (x == y) continue;
fa[x] = y;
sum += t.w;
if (++cnt == n - 1) break;
}
if (cnt == n - 1) cout << sum << '\n';
else cout << "Orz\n"; // ✗ 题面写的是 orz,全小写
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★ 它和上一个 bug 的抓获数完全相同 —— 因为触发条件是同一句话
300 轮 档 0 先造树 档 1 随机撒 档 2 保证不连通
忘了判 n − 1 0 208 300
orz 打成 Orz 0 208 300

两行逐格相同:它们都只在「图不连通」那些轮里现形,而那正是 208 / 300 那一档。

⇒ ★★ 这一条只有逐字节比才抓得到 —— 前面很多页都提过「对拍别 strip()」 (P1177 那次一 strip 就漏了 307 轮),这一页是它最干净的形态: 两版的差别只有一个字母的大小写。

4⚠ 并查集:fa[x] 是「爸爸」,不是「祖宗」

p3366Fa.cpp✗ 不 find,直接比 fa[u] 和 fa[v](官方样例照样打出 7)
// ✗ P3366:并查集不 find,直接比 fa[x] 和 fa[y]
//
// 这是本章第 6 步点过的那个误解在真题上的价钱:**fa[x] 是「爸爸」,不是「祖宗」**。
// ⚠ 而它在官方样例上**打出的就是标准答案 7** —— 三条边一条不多一条不少。
// 理由能说清楚:那组数据里被合并的树一直只有两层,爸爸恰好就是祖宗。
#include <bits/stdc++.h>
using namespace std;
int fa[5005];
struct E { int u, v, w; };
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
vector<E> e(m);
for (auto& t : e) cin >> t.u >> t.v >> t.w;
sort(e.begin(), e.end(), [](const E& a, const E& b) { return a.w < b.w; });
for (int i = 1; i <= n; i++) fa[i] = i;
int sum = 0, cnt = 0;
for (const auto& t : e) {
if (fa[t.u] == fa[t.v]) continue; // ✗ 比的是爸爸,不是祖宗
fa[t.u] = fa[t.v]; // ✗ 改的也只是自己那一个格子
sum += t.w;
if (++cnt == n - 1) break;
}
if (cnt == n - 1) cout << sum << '\n';
else cout << "orz\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

本章第 6 步专门点过这个误解。这一页给它称重量。

★ 为什么官方样例放过了它 —— 那组数据里树一直只有两层

样例的边按权排完是 (1,2,2) (1,3,2) (1,4,3) (3,4,3) (2,3,4)。前三条各连进一个新点, fa 被改成 fa[1]=2 → fa[1]=3 → fa[1]=4每一步比的那两个格子恰好就是各自的祖宗, 三条边收完 cnt 就够了 n − 1 = 3,输出 7。

⇒ 又一次那条规律样例筛掉的是「每组都错」的,放过的是「偶尔才错」的。

★ 触发条件 99、真被抓 94 —— 这一次两层挨得很近

它出错要两步:① 出现「两个点真在同一棵树里,可 fa 那一格看不出来」; ② 而且这一步真的改变了最后那个和。

档 0 · 300 轮
第一层(fa[u] != fa[v] 但其实同树)至少出现一次 99
真被抓 94

★ 比值 1.05 倍。⇒ 这条曲线本书已经量到从 1.0 倍P2240) 一直到 150 倍B3637)都有 —— 只能量,不能推。

5★★★ 排序反了:说清楚它「算了什么」

p3366Sort.cpp✗ 比较函数写成 a.w > b.w(样例打出 10,当场挡住)
// ✗ P3366:排序的比较函数写反了(从大到小)
//
// ★ 本章第 6 步那条恒等式:它不是「算错了」,它**精确地在解最大生成树**
// —— 见 p3366Count.cpp 的 maxst 那一行,300 组逐个相等。
// ⇒ 说清楚一个 bug「算了什么」之后,它的所有表现都是白送的推论:
// 所有边权都相同的图上它是对的,别的图上它每一组都错(⇒ 官方样例当场挡住)。
#include <bits/stdc++.h>
using namespace std;
int fa[5005];
int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); }
struct E { int u, v, w; };
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
vector<E> e(m);
for (auto& t : e) cin >> t.u >> t.v >> t.w;
sort(e.begin(), e.end(), [](const E& a, const E& b) { return a.w > b.w; }); // ✗ 反了
for (int i = 1; i <= n; i++) fa[i] = i;
int sum = 0, cnt = 0;
for (const auto& t : e) {
int x = find(t.u), y = find(t.v);
if (x == y) continue;
fa[x] = y;
sum += t.w;
if (++cnt == n - 1) break;
}
if (cnt == n - 1) cout << sum << '\n';
else cout << "orz\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★★ 它不是「算错了」—— 它精确地在解最大生成树

度量程序拿一份独立写的最大生成树(朴素 Prim,取 max 而不是 min, 和 Kruskal 一行代码都不共享)逐组去比:

档 0 · 300 组
「排序反了」的输出 ≡ 独立算的最大生成树 300 / 300

⇒ 说清楚它算了什么之后,它的所有表现都是白送的推论

  • 所有边权都相同的图上,最大生成树 = 最小生成树 ⇒ 它是对的(档 3 实测精确的 0);
  • 别的图上它几乎每组都错(档 0 抓 296 / 300)⇒ 官方样例当场挡住(10 ≠ 7);
  • 图不连通时它照样收不满 n − 1 条 ⇒ 也输出 orz(下一段那件事)。

★ 这是本章第 6 步那条恒等式在真题上的兑现,也是又一次 「说清楚一个 bug 算了什么,比说它错了有用得多」。

⚠⚠ 档 2 那个 0 是假的 —— 那 300 轮它在验零

表格里「排序反了」在 档 2(保证不连通) 上是 精确的 0。 差一点就要写成「不连通的图挡住了这个 bug」,而真相是:

档 2 · 300 组
正解本来就输出 orz 300 / 300

⇒ 两版一起打出 orz,对拍记「通过」,而验的是零

★ 这是「一致有两种:都算对了,和都没算」的又一次现场 —— ⚠ 而它和第 28 章 P2704 那次轮数是假的:全平原时 300 轮只有一组不同的输入) 是两种不同的假:那次是输入退化,这次是答案退化成了常量

6★★ 答案对,但跑不完 —— 而顶格随机一眼都看不出来

p3366Naive.cpp⚠ 并查集写对了,只是没有路径压缩(对拍一万轮也抓不到)
// ⚠ P3366:并查集写对了,只是**没有路径压缩**(find 老老实实往上爬)
//
// ★★ 这一版的答案**永远是对的** —— 对拍一万轮也抓不到它。
// 它坏的不是答案,是复杂度:单次 find 最坏 O(n),而这道题有 2 × 10⁵ 条边。
// ⇒ 「答案对但跑不完」只能靠**数次数 + 造对形状**发现([P5019] 那条)。
// 造法见 p3366Gen.cpp 档位 4:先用最轻的那批星形边把并查集**拉成一条链**
// (`fa[find(x)] = find(y)` 每次都把整棵树挂到一个新点下面),
// 再让剩下的十几万条「两头已经在同一棵树里」的边,每一条都从链底爬到链顶。
// ⚠ 关键是**最后一条边留到最重** —— 否则树提前长成,那句 break 会救了它。
#include <bits/stdc++.h>
using namespace std;
int fa[5005];
int find(int x) { // ⚠ 少的就是 `fa[x] =` 那半句
while (fa[x] != x) x = fa[x];
return x;
}
struct E { int u, v, w; };
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
vector<E> e(m);
for (auto& t : e) cin >> t.u >> t.v >> t.w;
sort(e.begin(), e.end(), [](const E& a, const E& b) { return a.w < b.w; });
for (int i = 1; i <= n; i++) fa[i] = i;
int sum = 0, cnt = 0;
for (const auto& t : e) {
int x = find(t.u), y = find(t.v);
if (x == y) continue;
fa[x] = y;
sum += t.w;
if (++cnt == n - 1) break;
}
if (cnt == n - 1) cout << sum << '\n';
else cout << "orz\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★★ 「顶格 ≠ 最坏」在这一页差 90 倍

find 往上爬了多少步 —— 一把机器无关的尺子:

顶格 n = 5000 / m = 2 × 10⁵ 没有路径压缩 有路径压缩 倍数
顶格随机(顺手造的那种) 21 868 994 143 687 152 倍
★★ 卡链档(下面那张造法) 1 961 527 496 414 986 4726 倍

同样是顶格,换个形状,朴素版的工作量差 89.7 倍。

秒表印证(A 机 · WSL2 · 8 核 / 7 G · 2026-08-31 · 独占 · 5 次取最小):

Kruskal(有压缩) ⚠ 没有路径压缩
顶格随机 30 毫秒 50 毫秒 ← 看着完全没问题
★ 卡链档 27 毫秒 2842 毫秒(时限 1 秒)

⇒ ★★★ 顺手造一组顶格随机跑一遍,这个 bug 一眼都看不见 (这已经是 栽在同一件事上了: 大不够,还要形状对)。

★ 卡链档是怎么造出来的 —— 而它有一处不留神就白造

fa[find(x)] = find(y) 这种写法,每次都把整棵树挂到另一个根下面。 于是喂给它一串星形边 (1,2) (1,3) (1,4) …,并查集会长成一条

(1,2) ⇒ fa[1]=2                   链:1 → 2
(1,3) ⇒ find(1)=2,fa[2]=3        链:1 → 2 → 3
(1,4) ⇒ find(1)=3,fa[3]=4        链:1 → 2 → 3 → 4

链拉好之后,再压十几万条「两头已经在同一棵树里」的边 —— 每一条都要从链底爬到链顶。

⚠⚠ 而这里有一个不留神就白造的地方:Kruskal 收够 n − 1 条就 break。 如果那条「最后一根救命边」排在前面,树提前长成,那句 break 会把朴素版救了。 ⇒ 所以生成器把最后那条边的权值设成 10⁴(全场最重),逼它跑完十几万条冗余边

7★ 另一条路:堆优化 Prim,以及一道该先算再量的账

p3366Prim.cpp★ 堆优化 Prim —— 也能 AC
// ★ P3366 的另一条路:堆优化 Prim —— 它也能 AC
//
// 本章第 7 步那句话:它和第 32 章的堆优化 Dijkstra **只差一个字**
// (Dijkstra 比的是 `dist[u] + w`,Prim 比的就是 `w`)。
// ⚠ 而这道题上它**比 Kruskal 慢**:n = 5000 / m = 2 × 10⁵ 是一张**稀疏图**,
// Kruskal 那一遍 sort 又快又便宜。实测见本页第 ⑥ 步那张表。
#include <bits/stdc++.h>
using namespace std;
using pii = pair<int, int>; // ⚠ (边权, 点号) —— 和第 32 章堆里那个顺序一样
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
vector<vector<pii>> g(n + 1); // g[u] 里存 (邻居, 边权)
for (int i = 0; i < m; i++) {
int u, v, w;
cin >> u >> v >> w;
g[u].push_back({v, w});
g[v].push_back({u, w}); // 无向图,存两遍
}
vector<char> in(n + 1, 0);
priority_queue<pii, vector<pii>, greater<pii>> q;
q.push({0, 1}); // 从 1 号点开始长
int sum = 0, cnt = 0;
while (!q.empty()) {
auto [w, u] = q.top();
q.pop();
if (in[u]) continue; // 已经在树里了,这条是过期货
in[u] = 1;
sum += w;
cnt++;
for (auto [v, c] : g[u]) if (!in[v]) q.push({c, v});
}
if (cnt == n) cout << sum << '\n'; // ⚠ Prim 数的是**点**,够 n 个才连通
else cout << "orz\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★ 这张表如果不量「只读入」那一列,会得出完全错误的结论

本章第 12 步自己写着这句话。这一页就是它的现场。

顶格输入 2 889 103 字节(2.76 MB),光读进来就要一笔钱:

A 机 · WSL2 · 2026-08-31 · 独占 · 顶格随机 · 5 次取最小 端到端 减掉读入
只读入cin + 关同步) 19 毫秒 ——
Kruskal 30 毫秒 11 毫秒
堆优化 Prim 49 毫秒 30 毫秒

⇒ 端到端看是 1.6 倍,减掉读入才是 ★ 2.7 倍 —— 两个结论差了将近一倍。

★ 顺带把读入那笔账也做完:不关同步cin75 毫秒(比关同步慢 3.9 倍), 光读入就吃掉 7.5% 的时限。 ⇒ 这道题四种读法都够,但「关不关同步」这一行值 56 毫秒,顺手写上不亏

⚠ 一条被实测打回来的草稿:朴素 Prim 在这道题上并没有 MLE

草稿里写的是「n = 5000 ⇒ 邻接矩阵必然爆内存,朴素 Prim 想都别想」。乘一遍就知道那句话不对

5001 × 5001 × 4 字节 95 MB
题面给的内存 128 MB
⇒ 余量 1.34 倍

⇒ 它放得下O(n²) = 2.5 × 10⁷ 也跑得完。正确的说法是 「能过,但余量只有 1.34 倍,而这题 m 只有 2 × 10⁵、本来就是稀疏图」—— ⇒ 选 Kruskal 的理由不是「朴素 Prim 会挂」,是没必要冒那个险。 ★ 而下一道 P1546n 只有 100,同一张矩阵只要 40 KB —— 「稠密图用朴素 Prim」这句话的主语从来是 n

8★ 两条一乘就完的算术,和一句噪声

★ 要不要 long long:不用,余量 42 倍
答案上界 (n − 1) × Z 4999 × 10⁴ = 49 990 000
int 上限 2 147 483 647
⇒ 余量 42 倍

⇒ 这是「答案 ≥ 任何一个被用到的中间值」那条论证模式的又一次 —— Kruskal 的 sum 只会一路加到答案,中途不会超过它。

★ 题面没排除自环(`1 ≤ Xᵢ, Yᵢ ≤ N`)—— 而它是噪声

轮连着两次撞上「顺手跳过自环」这件事,所以这一页也量了一遍:

档 0 + 档 1,共 600 组
输入里有自环 367 组
「特判 u == vcontinue」和「不特判」结论不同 0 组

★ 而这个 0 不是空壳:367 组是真的有自环。理由一行能证 —— 自环的两头 find(u) == find(v) 恒成立,Kruskal 的判环那一句已经把它挡掉了

⇒ 这是「这句约束重不重要,是「题目 × 你写的那一版」的属性」的又一次: 换成邻接矩阵存图,自环就会占掉 g[u][u] 那一格 —— 同一句话,换个写法就换一个答案。

9★ 对拍这一页

参照物是枚举所有边的子集:挑 n − 1 条边、检查连通、取权和最小的那一套 —— 既不是 Kruskal 也不是 Prim,直接照「生成树」三个字的定义走。

p3366Brute.cpp参照物:枚举所有边子集(300 轮不一致 0 轮)
300 轮(n 随机 5~7) 档 0 先造树 档 1 随机撒 档 2 保证不连通 档 3 边权全相同
Kruskal(正解) 0 0 0 0
堆优化 Prim 0 0 0 0
忘了判 n − 1 0 208 300 0
orz 打成 Orz 0 208 300 0
比爸爸不比祖宗 94 89 44 0
排序反了 296 75 0 0
没有路径压缩 0 0 0 0
★★ 这张表里有四种不同性质的 0,别把它们混成一句「对拍抓不到」
那个 0 性质 怎么办
档 0 的「忘了判 n − 1」/「Orz 生成器缺一档(图永远连通) 换生成器 ⇒ 300
档 3 的「排序反了」 能证的 0(边权全等 ⇒ 最大 = 最小生成树) 这一档本来就是它的自检
档 2 的「排序反了」 在验零(300 轮正解都输出 orz 别信它,换档
档 3 的「比爸爸不比祖宗」 能证的 0(边权全等 ⇒ 收哪 n − 1 条,和都一样) 同上
「没有路径压缩」四档全 0 ★★ 它压根不改答案 只能数次数 + 造对形状

⚠ 而「档 3 全是 0」本身不能自证「一个反例都没有」和「这段代码没在跑」输出一模一样)—— 这一页的自检是同一张表的另外三列:同一段对拍代码在档 0 / 1 / 2 上抓到了 94 / 208 / 300, 代码是活的。

★ 官方样例这一轮又当了一次「一测就死」的过滤器
错法 抓获率(档 0 / 档 1) 官方样例挡住了吗
排序反了 296 / 300 挡住了(打出 10)
比爸爸不比祖宗 94 / 89 放过了(打出 7)
忘了判 n − 1 0 / 208 放过了(样例是连通的)
orz 打成 Orz 0 / 208 放过了(同上)
没有路径压缩 0 / 0 放过了(答案永远对)

⇒ 五个里只挡住一个,而挡住的正是唯一那个「几乎每组都错」的。 ★ 后面四个各有各的放过理由,而只有第一个是「概率低」,另外三个都是结构性的

10度量程序和生成器

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

11一页纸

★ 关键的一步 Kruskal 照抄本章第 6 步;难的是最后那半句「不连通输出 orz
★★★ 那个 0 「先造树保证连通」的生成器让它变成精确的 0抓不到它的是档位,不是轮数
★★ 208 ≡ 208 触发条件和抓获数一个不差,而且能证(不连通 ⟺ 正解输出 orz
Orz 唯一一个和算法无关的错法,抓获数和上一行逐格相同对拍别 strip
★★★ 排序反了 精确地在解最大生成树(300 / 300)⇒ 所有表现都是白送的推论
⚠⚠ 档 2 那个 0 是假的 —— 那 300 轮正解也输出 orz在验零
★★★ 没有路径压缩 答案永远对 ⇒ 对拍抓不到;顶格随机 2187 万步(50 毫秒),卡链档 19.6 亿步(2842 毫秒)
★★ 选型 端到端 Kruskal 30 ms / 堆优化 Prim 49 ms;★ 减掉 19 ms 的读入才是真差距:11 vs 30
★ 算术 int 够(余量 42 倍);⚠ 朴素 Prim 的矩阵 95 MB / 128 MB,没爆,但余量只有 1.34 倍