0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1330,日期见页头。两边不一致时信原站。
题目描述
曹是一只爱刷街的老曹,暑假期间,他每天都欢快地在阳光大学的校园里刷街。 河蟹看到欢快的曹,感到不爽。河蟹决定封锁阳光大学,不让曹刷街。
阳光大学的校园是一张由 n 个点构成的无向图,n 个点之间由 m 条道路连接。
每只河蟹可以对一个点进行封锁,当某个点被封锁后,与这个点相连的道路就被封锁了,曹就无法在这些道路上刷街了。
非常悲剧的一点是,河蟹是一种不和谐的生物,当两只河蟹封锁了相邻的两个点时,他们会发生冲突。
询问:最少需要多少只河蟹,可以封锁所有道路并且不发生冲突。
输入格式
第一行两个正整数 n, m,表示节点数和边数。
接下来 m 行,每行两个整数 u, v,表示点 u 到点 v 之间有道路相连。
输出格式
仅一行,如果河蟹无法封锁所有道路,则输出 Impossible,否则输出一个整数,表示最少需要多少只河蟹。
说明/提示
【数据规模】对于 100% 的数据,1 ≤ n ≤ 10⁴,1 ≤ m ≤ 10⁵,保证没有重边。
输入输出样例
输入
3 3 1 2 1 3 2 3
输出
Impossible
三角形 1-2-3-1 是一个奇环:不管怎么涂,总有一条边两端同色 ⇒ Impossible。
输入
3 2 1 2 2 3
输出
1
一条链 1-2-3:只封锁 2 号点,两条路都被封住,而且没有两只河蟹相邻 ⇒ 答案 1。
⚠ 这两组样例合起来一个错法都没挡住 —— 本页三个错法在它们身上原样打出正确答案。
1★★ 先把题面翻译成一句话:每条边恰好一个端点被选
- 「封锁所有道路」= 每条边至少一个端点被选;
- 「不发生冲突」= 每条边至多一个端点被选(相邻两个都被选就冲突)。
⇒ 合起来:每条边恰好一个端点被选 —— 这正是「把点染成黑白两色、每条边两端异色」, 也就是一次二分图染色。
于是:
- 某个连通块染不出来(碰到相邻同色)⇒ 有奇环 ⇒ 全局
Impossible; - 染得出来,这个块就在「黑」和「白」里选人少的那一半 ⇒ 加上
min(黑, 白)。
★★ 各个连通块互不影响 —— 这个块选黑、那个块选白,完全合法。 所以是每块各取 min 再求和,不是最后全局取一次 min(第 ③ 步就是这个错法)。
// ★★ P1330 正解:**二分图染色,每个连通块各自取 min(黑, 白)**。//// 先把题目翻译成一句话:// 「封锁所有道路」= 每条边**至少**一个端点被选;// 「不发生冲突」 = 每条边**至多**一个端点被选;// ⇒ 合起来:**每条边恰好一个端点被选** ⇒ 这就是一个二分图染色。//// 于是:// · 一个连通块染成黑白两色,冲突(相邻同色)⇒ 有奇环 ⇒ **Impossible**;// · 染得成,就在这个块里取 **min(黑数, 白数)**;// · ★★ **各个连通块互不影响,所以是「每块各取 min 再求和」**,不是全局取 min。//// ⚠ 题面**没有保证图是连通的** —— 只从 1 号点搜是本页第一个错法。// ⚠ 题面只保证「没有重边」,**一个字都没说没有自环**。自环意味着一个点和自己相邻// ⇒ 必然 Impossible,而这一版天然处理得了(v == u 且已染色同色)。见 p1330Self.cpp。//// 复杂度 O(n + m)。顶格 n = 10⁴、m = 10⁵。孤立点自成一块,min(1, 0) = 0,白送。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n, m; if (!(cin >> n >> m)) return 0; vector<vector<int>> g(n + 1); for (int i = 0; i < m; i++) { int u, v; cin >> u >> v; g[u].push_back(v); g[v].push_back(u); // 无向图,两遍 }
vector<int> col(n + 1, -1), st; long long ans = 0; for (int s = 1; s <= n; s++) { if (col[s] != -1) continue; // ★ 每个连通块都要来一次 int cnt[2] = {0, 0}; st.clear(); st.push_back(s); col[s] = 0; while (!st.empty()) { int u = st.back(); st.pop_back(); cnt[col[u]]++; for (int v : g[u]) { if (col[v] == -1) { col[v] = col[u] ^ 1; st.push_back(v); } else if (col[v] == col[u]) { printf("Impossible\n"); return 0; } } } ans += min(cnt[0], cnt[1]); // ★★ 每块各取 min } printf("%lld\n", ans); return 0;}点「运行 ▶」看结果
对拍的参照物是枚举所有子集,逐字照题面那两句话验,和「染色」一行代码都不共享:
2⚠ 错法一:只从 1 号点搜一次 —— 题面从没说图是连通的
// ✗ P1330 错法一:**只从 1 号点搜一次** —— 忘了图可能不连通。//// 题面从头到尾没说「阳光大学的校园是连通的」。一旦有第二个连通块,// 它的道路根本没被封锁,答案偏小(而且它连那个块里有没有奇环都不知道)。//// ★ 恒 ≤ 正解(漏掉的块只会让答案更小),⚠ 而且它**可能把 Impossible 漏报成一个数**。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; if (!(cin >> n >> m)) return 0; vector<vector<int>> g(n + 1); for (int i = 0; i < m; i++) { int u, v; cin >> u >> v; g[u].push_back(v); g[v].push_back(u); }
vector<int> col(n + 1, -1), st; int cnt[2] = {0, 0}; st.push_back(1); col[1] = 0; // ★ 只有 1 号点 while (!st.empty()) { int u = st.back(); st.pop_back(); cnt[col[u]]++; for (int v : g[u]) { if (col[v] == -1) { col[v] = col[u] ^ 1; st.push_back(v); } else if (col[v] == col[u]) { printf("Impossible\n"); return 0; } } } printf("%d\n", min(cnt[0], cnt[1])); return 0;}点「运行 ▶」看结果
它是本页抓获率最高的错法(144 / 300),可两组官方样例都放过了它(样例里 n = 3 且连通)。
★ 而且它不只是答案偏小 —— 奇环长在别的连通块里时,它连 Impossible 都会漏报。
3⚠ 错法二:黑白全局累加,最后才取一次 min
// ✗ P1330 错法二:每个连通块都搜了,可**黑白是全局累加的,最后才取一次 min**。//// 这等于强行要求「所有块都选同一种颜色」。而各个块**互不影响** ——// 这个块选黑、那个块选白,完全合法。//// ★ 它给出的是一个**真做得到的方案**(每块都选同一色仍然合法)⇒ 答案**恒 ≥ 正解**。// ⇒ 这正是[第 26 章那条判据](/sol/p1220/):给出一个合法方案 ⇒ 恒 ≥ 最优。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; if (!(cin >> n >> m)) return 0; vector<vector<int>> g(n + 1); for (int i = 0; i < m; i++) { int u, v; cin >> u >> v; g[u].push_back(v); g[v].push_back(u); }
vector<int> col(n + 1, -1), st; long long tot[2] = {0, 0}; // ★ 全局两个桶 for (int s = 1; s <= n; s++) { if (col[s] != -1) continue; st.clear(); st.push_back(s); col[s] = 0; while (!st.empty()) { int u = st.back(); st.pop_back(); tot[col[u]]++; for (int v : g[u]) { if (col[v] == -1) { col[v] = col[u] ^ 1; st.push_back(v); } else if (col[v] == col[u]) { printf("Impossible\n"); return 0; } } } } printf("%lld\n", min(tot[0], tot[1])); // ★ 最后才取一次 min return 0;}点「运行 ▶」看结果
它等于强行要求「所有连通块都选同一种颜色」。
★ 它给出的是一个真做得到的方案(每块都挑同一色仍然合法)⇒ 答案恒 ≥ 正解。 ⇒ 这就是第 26 章那条判据:给出一个合法方案 ⇒ 恒 ≥ 最优;解一个放宽的问题 ⇒ 恒 ≤ 最优。 对拍被抓 40 / 300。
4★★★ 错法三:顺手写的那句「跳过自环」—— 而题面一个字都没说没有自环
// ✗ P1330 错法三:染色时**顺手跳过自环**(`if (v == u) continue;`)。//// 看着像一句无害的防御 —— 很多模板里都有。可这道题里它是致命的:// 一个自环意味着「这个点和自己相邻」⇒ 它被封锁就冲突、不封锁就漏了一条路// ⇒ **答案必然是 Impossible**,而跳过它就把这件事咽了下去,照常打出一个数。//// ⚠⚠ 而题面只写了「保证**没有重边**」—— **一个字都没说没有自环**。// 解析页第 ⑤ 步把这两件事分开量了一遍:// **重边对每一个版本都毫无影响(噪声),自环是这个写法的命门。**
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; if (!(cin >> n >> m)) return 0; vector<vector<int>> g(n + 1); for (int i = 0; i < m; i++) { int u, v; cin >> u >> v; g[u].push_back(v); g[v].push_back(u); }
vector<int> col(n + 1, -1), st; long long ans = 0; for (int s = 1; s <= n; s++) { if (col[s] != -1) continue; int cnt[2] = {0, 0}; st.clear(); st.push_back(s); col[s] = 0; while (!st.empty()) { int u = st.back(); st.pop_back(); cnt[col[u]]++; for (int v : g[u]) { if (v == u) continue; // ★ 就是这一句 if (col[v] == -1) { col[v] = col[u] ^ 1; st.push_back(v); } else if (col[v] == col[u]) { printf("Impossible\n"); return 0; } } } ans += min(cnt[0], cnt[1]); } printf("%lld\n", ans); return 0;}点「运行 ▶」看结果
自环意味着「这个点和自己相邻」:封锁它就和自己冲突,不封锁它这条路就没被封 ⇒ 两句话同时满足不了。
⚠⚠ 而题面写的是「保证没有重边」—— 一个字都没说没有自环。 造一档带自环的数据(题面允许!):
| 300 轮,允许自环 | |
|---|---|
| 真的有自环的轮数 | 132 |
| 「跳过自环」被抓 | 118 |
| ★ 差的那 14 轮 | 图里本来还有别的奇环 ⇒ 它照样打出了 Impossible |
⇒ 132 = 118 + 14,一轮不多一轮不少。
★ 这又是「满足触发条件 ↔ 真被抓」那件事:这次差 14 轮,而差的原因能一条条数清楚。
(本书量过的比值从「一个不差」到「差 150 倍」都有 —— 只能量,不能推。)
5★★★ 而题面那句真正写出来的保证(「没有重边」)是噪声 —— 实测零影响
| 300 轮,允许重边 | |
|---|---|
| 真的有重边的轮数 | 76 |
| 把重边去掉之后,四个版本的输出一共变了 | ★ 0 次 |
理由一行就能说清:染色只问「这两个点是不是异色」, 同一对点说十遍和说一遍,问出来的是同一件事。
⇒ 按第 12 章那套三分法,题面那句「保证没有重边」是噪声。
⚠ 于是本章题单里那句「它的数据里有重边和自环的坑」要订正: 重边一点坑都没有(实测零影响,而且题面已经把它排除了); 真正没被排除、也真的会咬人的是自环,以及图不保证连通。 ⇒ ★ 这是「本章原题的解析页该回去查一遍正文 / 题单」的又一次 —— 题单那句话已经改了。
6⚠ 一个被实测打回来的猜想:「随机图里大半轮是 Impossible,抓获率会被稀释」
顺手写的生成器造出来的随机图几乎必有奇环 —— 实测 300 轮里 68 轮答案就是 Impossible。
本书踩过好几次「一致有两种:都算对了,和都没算」,
所以我先写下的草稿是:换成「保证是二分图」的生成器,抓获率应该会明显上去。
| 300 轮 | 顺手随机图 | 保证是二分图 |
|---|---|---|
答案是 Impossible 的轮数 |
68 | ★ 0 |
| 只从 1 号点被抓 | 144 | 135 |
| 全局取 min 被抓 | 40 | 43 |
⇒ ★★ 那 68 轮并没有「验的是零」 —— 恰恰相反:奇环长在别的连通块里时,
「只从 1 号点」会把 Impossible 漏成一个数字,反而更容易露馅。
★★★ 「一致有两种」是一条提醒,不是一条定理。 它说的是「你可能在验零,去数一数」,
而不是「答案退化成常量的那些轮一定没用」。这一页数完发现:它们照样在干活。
(对照第 14 章 P1746:那次 165 / 300 轮两版一起输出 -1,抓获率是真被稀释了。)
7★ 规模:一道三十秒的算术题
顶格 n = 10⁴、m = 10⁵ |
|
|---|---|
| 无向边存两遍 ⇒ 有向边 | 200 000 |
vector 邻接表 |
约 0.99 MB(题面给 128 MB) |
| 复杂度 | O(n + m) = 1.1 × 10⁵ |
| 顶格实测(A 机 · WSL2 · 2026-08-30,独占) | 0.01 秒 / 5.9 MB(时限 1 秒) |
★ 顺带:n ≤ 10⁴ ⇒ 递归染色最多 10⁴ 层,这道题上递归是安全的
(P5318 量过:函数体精简的递归门槛在 17.4 万层)。本页仍然写成迭代 —— 不用赌就别赌。
8★ 对拍这一页
300 轮(n 随机 4~10,参照物 = 2ⁿ 枚举子集) |
随机图 | 保证二分图 | 允许自环 | 允许重边 |
|---|---|---|---|---|
| 正解 | ★ 0 | ★ 0 | ★ 0 | ★ 0 |
| 只从 1 号点 | 144 | 135 | —— | —— |
| 全局取 min | 40 | 43 | —— | —— |
| 跳过自环 | 0(这档没自环) | —— | ★ 118 | —— |
| 四个版本因重边而变 | —— | —— | —— | ★ 0 |
9度量程序和生成器
10一页纸
| ★★ 关键的一步 | 「封锁所有」+「不冲突」= 每条边恰好一端被选 ⇒ 二分图染色 |
| ★★ 第二关键 | 各连通块互不影响 ⇒ 每块各取 min(黑, 白) 再求和 |
| ⚠ 错法一 | 只从 1 号点搜(图不保证连通)—— 抓获率最高 144/300,可样例放过 |
| ⚠ 错法二 | 全局取一次 min ⇒ 逼所有块同色 ⇒ 恒 ≥ 正解(40/300) |
| ★★★ 错法三 | 顺手那句「跳过自环」—— 题面只保证没重边;有自环 132 轮 = 被抓 118 + 别的奇环 14 |
| ★★★ 题面那句保证 | 「没有重边」实测零影响(噪声)⇒ 题单那句「重边的坑」已订正 |
| ⚠ 草稿被打回 | 「Impossible 那 68 轮在验零」是错的 —— 换成二分图档,抓获率 144 → 135、40 → 43 |
| ★ 两组样例 | 一个错法都没挡住(那条「样例是一测就死的过滤器」的另一个极端) |