题单 · 习题解析

洛谷 P1330 封锁阳光大学

★★ 每条边恰好一端被选 ⇒ 二分图染色,**每个连通块各取 min** 再求和;★★★ 题面只保证「没有重边」(实测零影响 = 噪声),一个字没说没有自环(132 = 118 + 14);⚠ 草稿被打回:Impossible 那 68 轮并没有在「验零」,抓获率几乎没动

原题:洛谷 P1330出自 第 29 章 图的存储:三种存法的对比与选型 的题单题面本地存档:2026-08-30
⚠ 先自己写一遍,再往下看

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

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.cpp★★ 二分图染色 + 每块各取 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

对拍的参照物是枚举所有子集,逐字照题面那两句话验,和「染色」一行代码都不共享:

p1330Brute.cpp参照物:2ⁿ 枚举子集(300 轮不一致 0 轮)

2⚠ 错法一:只从 1 号点搜一次 —— 题面从没说图是连通的

p1330One.cpp✗ 忘了图可能不连通
// ✗ 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

它是本页抓获率最高的错法(144 / 300),可两组官方样例都放过了它(样例里 n = 3 且连通)。 ★ 而且它不只是答案偏小 —— 奇环长在别的连通块里时,它连 Impossible 都会漏报

3⚠ 错法二:黑白全局累加,最后才取一次 min

p1330Global.cpp✗ 全局取 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

它等于强行要求「所有连通块都选同一种颜色」。

★ 它给出的是一个真做得到的方案(每块都挑同一色仍然合法)⇒ 答案恒 ≥ 正解。 ⇒ 这就是第 26 章那条判据给出一个合法方案 ⇒ 恒 ≥ 最优;解一个放宽的问题 ⇒ 恒 ≤ 最优。 对拍被抓 40 / 300

4★★★ 错法三:顺手写的那句「跳过自环」—— 而题面一个字都没说没有自环

p1330Self.cpp✗ if (v == u) continue;
// ✗ 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★ 一个自环 ⇒ 必然 Impossible,而这句「防御」把它咽了下去

自环意味着「这个点和自己相邻」:封锁它就和自己冲突,不封锁它这条路就没被封 ⇒ 两句话同时满足不了

⚠⚠ 而题面写的是「保证没有重边」—— 一个字都没说没有自环。 造一档带自环的数据(题面允许!):

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度量程序和生成器

p1330Count.cpp度量程序(本页所有数字都出自它)
p1330Gen.cpp数据生成器

10一页纸

★★ 关键的一步 「封锁所有」+「不冲突」= 每条边恰好一端被选 ⇒ 二分图染色
★★ 第二关键 各连通块互不影响 ⇒ 每块各取 min(黑, 白) 再求和
⚠ 错法一 只从 1 号点搜(图不保证连通)—— 抓获率最高 144/300,可样例放过
⚠ 错法二 全局取一次 min ⇒ 逼所有块同色 ⇒ 恒 ≥ 正解(40/300)
★★★ 错法三 顺手那句「跳过自环」—— 题面只保证没重边;有自环 132 轮 = 被抓 118 + 别的奇环 14
★★★ 题面那句保证 「没有重边」实测零影响(噪声)⇒ 题单那句「重边的坑」已订正
⚠ 草稿被打回 「Impossible 那 68 轮在验零」是错的 —— 换成二分图档,抓获率 144 → 135、40 → 43
★ 两组样例 一个错法都没挡住(那条「样例是一测就死的过滤器」的另一个极端)