0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库 —— 图也要存。
转录自洛谷 P1332,日期见页头。两边不一致时信原站。
题目背景
巫妖王的天灾军团终于卷土重来,血色十字军组织了一支先锋军前往诺森德大陆对抗天灾军团, 以及一切沾有亡灵气息的生物。孤立于联盟和部落的血色先锋军很快就遭到了天灾军团的重重包围, 现在他们将主力只好聚集了起来,以抵抗天灾军团的围剿。可怕的是,他们之中有人感染上了亡灵瘟疫, 如果不设法阻止瘟疫的扩散,很快就会遭到灭顶之灾。大领主阿比迪斯已经开始调查瘟疫的源头。 原来是血色先锋军的内部出现了叛徒,这个叛徒已经投靠了天灾军团, 想要将整个血色先锋军全部转化为天灾军团!无需惊讶,你就是那个叛徒。 在你的行踪败露之前,要尽快完成巫妖王交给你的任务。
题目描述
军团是一个 n 行 m 列的矩阵,每个单元是一个血色先锋军的成员。
感染瘟疫的人,每过一个小时,就会向四周扩散瘟疫,直到所有人全部感染上瘟疫。
你已经掌握了感染源的位置,任务是算出血色先锋军的领主们感染瘟疫的时间,
并且将它报告给巫妖王,以便对血色先锋军进行一轮有针对性的围剿。
输入格式
第 1 行:四个整数 n,m,a,b,表示军团矩阵有 n 行 m 列。
有 a 个感染源,b 为血色敢死队中领主的数量。
接下来 a 行:每行有两个整数 x,y,表示感染源在第 x 行第 y 列。
接下来 b 行:每行有两个整数 x,y,表示领主的位置在第 x 行第 y 列。
输出格式
第 1 至 b 行:每行一个整数,表示这个领主感染瘟疫的时间,输出顺序与输入顺序一致。
如果某个人的位置在感染源,那么他感染瘟疫的时间为 0。
数据规模与约定
对于 100% 的数据,保证 1 ≤ n, m ≤ 500,1 ≤ a, b ≤ 10⁵。
输入输出样例
输入
5 4 2 3 1 1 5 4 3 3 5 3 2 4
输出
3 1 3
样例解释 —— 如下图,标记出了所有人感染瘟疫的时间以及感染源和领主的位置:

1第一反应:每个感染源各跑一次 BFS,取 min
BFS 会算出「一个起点到全图每一格」的距离,那 a 个感染源就跑 a 次、逐格取最小值 ——
想法直接,而且答案完全正确:
// ⚠ 第一版:每个感染源各跑一次 BFS,把结果取 min(**答案是对的,就是跑不完**)//// 这是最直接的想法 —— 「BFS 会算一个源到全图的距离,那 a 个源就跑 a 次呗」。//// ★ 它的答案**完全正确**,所以对拍一个字都说不出来(这一页第 ⑥ 步:精确的 0)。// 量它只能换一把尺子:**数一共入了多少次队**。//// 复杂度 O(a × nm):顶格 a = 10⁵、nm = 250000 ⇒ **2.5 × 10¹⁰** 次入队。// ⇒ 而多源 BFS 是 O(nm) = 250000 次,和 a 有多大**没有关系** —— 差 a 倍。//// ⚠ 这一版自带一道闸:入队次数超上限就报出来并退出,// 绝不「悄悄少跑几个源」把自己救活([P1443](/sol/p1443/) 那条教训)。
#include <bits/stdc++.h>using namespace std;
const int DX[4] = {-1, 1, 0, 0};const int DY[4] = {0, 0, -1, 1};const long long PUSH_LIMIT = 200000000LL;
int main() { int n, m, a, b; if (scanf("%d %d %d %d", &n, &m, &a, &b) != 4) return 0;
vector<pair<int, int>> src(a); for (auto& [x, y] : src) if (scanf("%d %d", &x, &y) != 2) return 0;
vector<vector<int>> best(n + 1, vector<int>(m + 1, INT_MAX)); vector<vector<int>> dist_(n + 1, vector<int>(m + 1)); long long pushes = 0;
for (auto [sx, sy] : src) { // ⚠ 一个源一次 BFS —— a 次 for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) dist_[i][j] = -1;
queue<pair<int, int>> q; dist_[sx][sy] = 0; q.push({sx, sy}); pushes++; while (!q.empty()) { auto [x, y] = q.front(); q.pop(); for (int k = 0; k < 4; k++) { int nx = x + DX[k], ny = y + DY[k]; if (nx < 1 || nx > n || ny < 1 || ny > m) continue; if (dist_[nx][ny] != -1) continue; dist_[nx][ny] = dist_[x][y] + 1; q.push({nx, ny}); if (++pushes > PUSH_LIMIT) { fprintf(stderr, "入队次数超过 %lld —— 这一档它根本跑不完\n", PUSH_LIMIT); return 1; } } } for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) best[i][j] = min(best[i][j], dist_[i][j]); }
for (int i = 0; i < b; i++) { int x, y; if (scanf("%d %d", &x, &y) != 2) return 0; printf("%d\n", best[x][y]); } return 0;}点「运行 ▶」看结果
它输在哪:每一次 BFS 都要把整张图铺一遍。
| 网格 × 感染源个数 | 多源 BFS 入队 | ⚠ 每源各一次 | 倍数 |
|---|---|---|---|
20 × 20,a = 1 |
400 | 400 | 1 |
20 × 20,a = 4 |
400 | 1600 | 4 |
20 × 20,a = 16 |
400 | 6400 | 16 |
50 × 50,a = 10 |
2500 | 25000 | 10 |
倍数正好就是 a。⇒ 顶格 n = m = 500、a = 10⁵:
多源 BFS 是 250000 次入队,这一版是 2.5 × 10¹⁰ 次。
入队次数超上限就报到 stderr 并 return 1,绝不「悄悄少跑几个源」把自己救活 ——
P1443 那条教训:演示用的慢版本,也不许改去演示另一件事。
2⚠ 一个「看着像优化」的写法:共用一个 dist 数组
上一版每个源都要把 dist 清一遍、再铺一遍全图,重复劳动明摆着。
于是很自然会想:已经算过距离的格子就别再算了呗 —— 把 dist 提到循环外面:
for (每个感染源 s)
if (dist[s] == -1) { dist[s] = 0; BFS(s); } // dist 不清空
// ⚠⚠ 错法:把「每个源各跑一次 BFS」顺手「优化」成共用一个 dist 数组//// 上一版(p1332Each.cpp)每个源都要把 dist 清一遍、跑一遍全图,慢得离谱。// 于是很自然会想:「已经算过距离的格子就别再算了呗」——// 把 dist 数组提到循环外面,`if (dist[nx][ny] != -1) continue;` 照旧://// for (每个感染源 s)// if (dist[s] == -1) { dist[s] = 0; BFS(s); } // ⚠ dist 不清空//// 看着像是把重复劳动省掉了,**而它算出来的根本不是「到最近感染源的距离」**://// ★ 第一个源的那趟 BFS 会把**整张图**铺满(网格是连通的,没有障碍);// 等轮到第二个源,`dist[s]` 早就不是 -1 了 —— 它一步都走不了。// ⇒ 结果**等于「只有第一个感染源」**,后面 a − 1 个源全部作废。//// ★ 草稿在这儿写过一句「而它在样例上照样能对」—— **实测把它打回来了**:// 官方样例三个询问它全给错(`4 6 4`,正确是 `3 1 3`),因为样例的第一个源在角上 (1,1),// 而三个询问点都离第二个源 (5,4) 更近。⇒ **这一版是样例就挡得住的**。// ⚠ 但别把「样例挡得住」当成它无害:只要输入里第一个源恰好离询问点最近,它就对了 ——// 到底占多大比例是量出来的,见 p1332Count.cpp 第 ③ 段。
#include <bits/stdc++.h>using namespace std;
const int DX[4] = {-1, 1, 0, 0};const int DY[4] = {0, 0, -1, 1};
int main() { int n, m, a, b; if (scanf("%d %d %d %d", &n, &m, &a, &b) != 4) return 0;
vector<vector<int>> dist_(n + 1, vector<int>(m + 1, -1));
for (int i = 0; i < a; i++) { int sx, sy; if (scanf("%d %d", &sx, &sy) != 2) return 0; if (dist_[sx][sy] != -1) continue; // ⚠⚠ 就是这一句:第二个源起全被挡在门外 dist_[sx][sy] = 0; queue<pair<int, int>> q; q.push({sx, sy}); while (!q.empty()) { auto [x, y] = q.front(); q.pop(); for (int k = 0; k < 4; k++) { int nx = x + DX[k], ny = y + DY[k]; if (nx < 1 || nx > n || ny < 1 || ny > m) continue; if (dist_[nx][ny] != -1) continue; dist_[nx][ny] = dist_[x][y] + 1; q.push({nx, ny}); } } }
for (int i = 0; i < b; i++) { int x, y; if (scanf("%d %d", &x, &y) != 2) return 0; printf("%d\n", dist_[x][y]); } return 0;}点「运行 ▶」看结果
官方样例当场就挡住了:正确是 3 / 1 / 3,它给 4 / 6 / 4。
网格是连通的、又没有任何障碍 ——
所以第一个源那趟 BFS 会把整张图铺满。等轮到第二个源,dist[s] 早就不是 -1 了,
它连门都出不去。第三个、第四个……全一样。
⇒ 这不是「少算了一点」,是后面 a − 1 个感染源全部作废。
把这句话量成断言:拿 n, m ≤ 10、a ≤ 5 的各种输入,
把这一版的结果和「只保留第一个感染源」的多源 BFS 逐格比:
| 比了多少格 | 一致的 |
|---|---|
| 15125 | ★ 15125(一个不差) |
⇒ 说清楚一个 bug「算了什么」,比说它「错了」有用得多 ——
知道它等于「只有第一个源」,下一句话(a = 1 时它是对的)就是白送的。
它错在「第二个源」。所以生成器只造一个感染源的话,它永远是对的:
感染源个数 a |
1 | 2 | 3 | 5 | 10 |
|---|---|---|---|---|---|
| 300 轮里被抓 | ★ 0 | 271 | 293 | 300 | 300 |
这是第 12 章 P1226 分的第二种「对拍 0 次」——
不是题面挡死了,是生成器缺一档:题面明明允许 a 到 10⁵。
⇒ 而这一档还特别容易漏:a = 1 是最小、最顺手的默认值。
3★ 多源 BFS:把所有感染源一起当第 0 层
// P1332 血色先锋队 —— 多源 BFS(★ 这一版就能 AC)//// 题目:n × m 的矩阵,给 a 个感染源;瘟疫每小时向上下左右扩散一格,// 问 b 个指定位置各在第几小时被感染。n, m ≤ 500,a, b ≤ 10⁵。//// ★ 关键的一步:**把 a 个感染源一开始就全塞进队列**,然后照常 BFS 一次。//// 为什么这样是对的:想象凭空加一个「超级源点」,它和每个感染源之间连一条长度 0 的边。// 从超级源点做 BFS,第 0 层就是全部感染源、第 1 层是它们的邻居……// ⇒ **BFS「按距离一圈一圈往外」这件事一点没变**,变的只是第 0 层不止一个格子。// ⇒ 每个格子第一次被碰到时,拿到的就是「到最近的那个感染源」的距离。//// 复杂度 O(nm):整张图只走一遍,和 a 有多大**没有关系**。// ⚠ 对比第 ① 版(每个源各跑一次 BFS):那是 O(a × nm),顶格 10⁵ × 250000 = 2.5 × 10¹⁰。//// ⚠ a 个感染源里**可能有重复坐标**(题面没保证互异)——「入队时就标记」天然挡掉了重复。
#include <bits/stdc++.h>using namespace std;
const int DX[4] = {-1, 1, 0, 0};const int DY[4] = {0, 0, -1, 1};
int main() { int n, m, a, b; if (scanf("%d %d %d %d", &n, &m, &a, &b) != 4) return 0;
vector<vector<int>> dist_(n + 1, vector<int>(m + 1, -1)); queue<pair<int, int>> q;
for (int i = 0; i < a; i++) { // ★ 所有感染源一起当第 0 层 int x, y; if (scanf("%d %d", &x, &y) != 2) return 0; if (dist_[x][y] != -1) continue; // 重复的源,跳过 dist_[x][y] = 0; q.push({x, y}); }
while (!q.empty()) { auto [x, y] = q.front(); q.pop(); for (int k = 0; k < 4; k++) { int nx = x + DX[k], ny = y + DY[k]; if (nx < 1 || nx > n || ny < 1 || ny > m) continue; if (dist_[nx][ny] != -1) continue; dist_[nx][ny] = dist_[x][y] + 1; // 入队时就定下距离 q.push({nx, ny}); } }
for (int i = 0; i < b; i++) { // b 个询问都是 O(1) 查表 int x, y; if (scanf("%d %d", &x, &y) != 2) return 0; printf("%d\n", dist_[x][y]); } return 0;}点「运行 ▶」看结果
改动只有一处:BFS 开始之前,把 a 个感染源全部塞进队列(距离都记 0)。
凭空加一个点 S,让它和每个感染源之间连一条长度 0 的边:
S
/ | \
0 0 0
/ | \
源1 源2 源3 <- 全在第 0 层
| | |
1 1 1 <- 它们的邻居全在第 1 层从 S 做 BFS:第 0 层就是全部感染源,第 1 层是它们的邻居,……
⇒ BFS「按距离一圈一圈往外」这件事一点没变,变的只是第 0 层不止一个格子。
每个格子第一次被碰到时拿到的,就是「到最近的那个感染源」的距离。
★ 复杂度 O(nm) —— 和 a 有多大完全没有关系。
a = 10⁵ 而网格只有 500 × 500 = 250000 格,
感染源里必然有大量重复坐标 —— 「入队时就标记」天然把它们挡掉了
(题面并没有保证感染源互不相同)。
4★★★ 验算走一条和算法完全无关的路:曼哈顿距离
这道题的网格没有任何障碍,四个方向都能走。在这种网格上,两点间的最短步数就是
|x1 - x2| + |y1 - y2|
⇒ 答案 = 对所有感染源取这个式子的最小值。一行公式,一个队列都不用:
// 验算用的第二条路:直接取「到最近感染源的曼哈顿距离」//// ★★★ 这道题的网格**没有任何障碍**,四个方向都能走。// 在这种网格上,从 (x1,y1) 走到 (x2,y2) 的最短步数就是//// |x1 - x2| + |y1 - y2|//// ⇒ 答案 = 对所有感染源取这个式子的最小值。**一行公式,一个队列都不用。**//// ⚠ 但它**不能当解法**:b 个询问 × a 个源 = 10⁵ × 10⁵ = **10¹⁰** 次减法,跑不完。// (这正是这道题要用 BFS 的理由 —— 不是因为公式不对,是因为规模不对。)//// ★ 那它有什么用?**当对拍的参照物。**// 它和多源 BFS **一行代码都不共享**,走的是完全不同的一条路 ——// 这正是[第 7 章 P1147](/sol/p1147/) 那条:「验算最好走一条和算法完全无关的路」。//// ⚠⚠ 而这条等价**是有前提的**:网格里不能有障碍。// 随便挖一堵墙,「最短步数 = 曼哈顿距离」立刻不成立(p1332Count.cpp 第 ④ 段量了一个反例)。// ⇒ 用一条捷径去验算之前,先问清楚**这条捷径的前提是什么**。
#include <bits/stdc++.h>using namespace std;
int main() { int n, m, a, b; if (scanf("%d %d %d %d", &n, &m, &a, &b) != 4) return 0; (void)n; (void)m;
vector<pair<int, int>> src(a); for (auto& [x, y] : src) if (scanf("%d %d", &x, &y) != 2) return 0;
for (int i = 0; i < b; i++) { int x, y; if (scanf("%d %d", &x, &y) != 2) return 0; int best = INT_MAX; for (auto [sx, sy] : src) best = min(best, abs(sx - x) + abs(sy - y)); printf("%d\n", best); } return 0;}点「运行 ▶」看结果
把它和多源 BFS 逐格比(n, m ≤ 12、a ≤ 4 的各种输入):
| 比了多少格 | 一致的 |
|---|---|
| 24336 | ★ 24336(一个不差) |
第 7 章 P1147 那条说的就是这件事: 验算用的第二条路,和被验的那条共享的代码越少越好。 这一页里两条路一行都不共享 —— 一个是队列 + 层序,一个是绝对值相减。
⚠ 而它不能当解法:b 个询问 × a 个源 = 10⁵ × 10⁵ = 10¹⁰ 次减法,跑不完。
⇒ 这道题要用 BFS,不是因为公式不对,是因为规模不对。
一个「更简单的公式」可以是完美的对拍参照物,同时完全不能提交 —— 这两件事不矛盾。
随便挖一堵墙,「最短步数 = 曼哈顿距离」立刻不成立。度量程序里那个具体反例:
. 1 . '1' 是墙:(1,2) 和 (2,2)
. 1 . 感染源在 (1,1),要到 (1,3)
. . .| 真实最短步数 | 曼哈顿距离 | |
|---|---|---|
(1,1) → (1,3) |
6 | ⚠ 2 |
⇒ 用一条捷径去验算之前,先问清楚这条捷径的前提是什么。 这道题恰好满足(题面里没有任何不可通行的格子),所以它成立; 换成第 13 章那种有水有陆地的网格,这条验算路子一句话都说不了。
5对拍:参照物是那条曼哈顿公式
// P1332 对拍生成器:`./p1332Gen <seed> [maxN] [maxA] [maxB]`//// 输出一整份输入:`n m a b`、a 行感染源、b 行询问。//// ★ 这一页最要紧的旋钮是 **a(感染源个数)**,默认 4:// ⚠ `a = 1` 时 `p1332Shared.cpp`(那个「共用 dist 数组」的错法)**是完全正确的** ——// 它错在「第二个源起全被挡在门外」,而根本没有第二个源。// ⇒ 只造一个感染源的生成器,对那个 bug 是**精确的 0**(p1332Count.cpp 第 ③ 段量了这条曲线)。//// 坐标允许重复(题面没保证感染源互异),正解靠「入队时就标记」天然挡掉。
#include <bits/stdc++.h>using namespace std;
int main(int argc, char** argv) { unsigned seed = argc > 1 ? (unsigned)atoi(argv[1]) : 1u; int maxN = argc > 2 ? atoi(argv[2]) : 8; int maxA = argc > 3 ? atoi(argv[3]) : 4; int maxB = argc > 4 ? atoi(argv[4]) : 6; if (maxN < 1) maxN = 1; if (maxA < 1) maxA = 1; if (maxB < 1) maxB = 1;
mt19937 rng(seed); int n = (int)(rng() % (unsigned)maxN) + 1; int m = (int)(rng() % (unsigned)maxN) + 1; int a = (int)(rng() % (unsigned)maxA) + 1; int b = (int)(rng() % (unsigned)maxB) + 1;
printf("%d %d %d %d\n", n, m, a, b); for (int i = 0; i < a + b; i++) printf("%d %d\n", (int)(rng() % (unsigned)n) + 1, (int)(rng() % (unsigned)m) + 1); return 0;}点「运行 ▶」看结果
300 轮,网格 ≤ 8 × 8,参照物是 p1332Manhattan.cpp:
生成器的 a 上限 |
1 | 2 | 4 | 10 |
|---|---|---|---|---|
★ p1332(多源 BFS) |
0 | 0 | 0 | 0 |
★ p1332Each(每源各一次) |
0 | 0 | 0 | 0 |
⚠⚠ p1332Shared(共用 dist) |
★ 0 | 99 | 171 | 224 |
p1332和p1332Each的0是「它们真的对」 —— 后者只是慢,P1746 那条「对拍抓不到的东西,先问它会不会体现在输出上」。p1332Shared在a = 1那一档的0是「生成器缺一档」 —— 它错得很彻底,只是那一档触发不到。
⇒ 同一张表里两种 0,分辨它们靠的不是轮数,是想清楚每一版错在哪。
6这一页所有数字都出自这一份
// P1332 的度量程序 —— 这一页所有数字都出自这一份。//// `./p1332Count` 人看的版本// `./p1332Count csv` 一行一项,给 scripts/check-viz.mjs 写断言用//// 五段:// ① 入队次数:多源 BFS 是 O(nm),「每个源各跑一次」是 O(a × nm) —— 正好差 a 倍;// ② ★★★ 无障碍网格上,多源 BFS 的结果**逐格等于**「到最近感染源的曼哈顿距离」;// ③ ⚠ 而这条等价**有前提**:随便挖一堵墙就不成立(给一个具体反例);// ④ 「共用 dist 数组」那个错法**究竟算了什么**:逐格等于「只有第一个感染源」;// 以及它的抓获率沿 a 走 —— **a = 1 那一档是精确的 0**;// ⑤ 顶格那一档的读入有多大。
#include <bits/stdc++.h>using namespace std;
static bool CSV = false;static const int DX[4] = {-1, 1, 0, 0};static const int DY[4] = {0, 0, -1, 1};
static void row(const char* key, const vector<long long>& v) { if (!CSV) return; printf("%s", key); for (long long x : v) printf(",%lld", x); printf("\n");}
/** 多源 BFS。wall = '1' 的格子不能走(这道题本身没有墙,第 ③ 段才用得上)。 */static pair<vector<vector<int>>, long long> multiBfs( int n, int m, const vector<pair<int, int>>& src, const vector<string>* wall = nullptr) { vector<vector<int>> d(n + 1, vector<int>(m + 1, -1)); queue<pair<int, int>> q; long long pushes = 0; for (auto [x, y] : src) { if (d[x][y] != -1) continue; d[x][y] = 0; q.push({x, y}); pushes++; } while (!q.empty()) { auto [x, y] = q.front(); q.pop(); for (int k = 0; k < 4; k++) { int a = x + DX[k], b = y + DY[k]; if (a < 1 || a > n || b < 1 || b > m) continue; if (wall && (*wall)[a - 1][b - 1] == '1') continue; if (d[a][b] != -1) continue; d[a][b] = d[x][y] + 1; q.push({a, b}); pushes++; } } return {d, pushes};}
/** 每个源各跑一次 BFS,取 min。只关心入队次数。 */static long long eachPushes(int n, int m, const vector<pair<int, int>>& src) { long long pushes = 0; for (auto [sx, sy] : src) { vector<vector<int>> d(n + 1, vector<int>(m + 1, -1)); queue<pair<int, int>> q; d[sx][sy] = 0; q.push({sx, sy}); pushes++; while (!q.empty()) { auto [x, y] = q.front(); q.pop(); for (int k = 0; k < 4; k++) { int a = x + DX[k], b = y + DY[k]; if (a < 1 || a > n || b < 1 || b > m) continue; if (d[a][b] != -1) continue; d[a][b] = d[x][y] + 1; q.push({a, b}); pushes++; } } } return pushes;}
/** 「共用 dist 数组」那个错法。 */static vector<vector<int>> sharedBfs(int n, int m, const vector<pair<int, int>>& src) { vector<vector<int>> d(n + 1, vector<int>(m + 1, -1)); for (auto [sx, sy] : src) { if (d[sx][sy] != -1) continue; d[sx][sy] = 0; queue<pair<int, int>> q; q.push({sx, sy}); while (!q.empty()) { auto [x, y] = q.front(); q.pop(); for (int k = 0; k < 4; k++) { int a = x + DX[k], b = y + DY[k]; if (a < 1 || a > n || b < 1 || b > m) continue; if (d[a][b] != -1) continue; d[a][b] = d[x][y] + 1; q.push({a, b}); } } } return d;}
int main(int argc, char** argv) { CSV = (argc > 1 && string(argv[1]) == "csv");
/* ① 入队次数:多源 vs 每个源各跑一次 */ { vector<long long> multi, each, ratio; vector<array<int, 3>> cases = {{20, 20, 1}, {20, 20, 4}, {20, 20, 16}, {50, 50, 10}}; if (!CSV) printf("① 入队次数(多源 BFS 一次铺满 vs 每个源各跑一次)\n"); for (auto [n, m, a] : cases) { mt19937 rng(12345u + (unsigned)a); vector<pair<int, int>> src; for (int i = 0; i < a; i++) src.push_back({(int)(rng() % (unsigned)n) + 1, (int)(rng() % (unsigned)m) + 1}); long long p1 = multiBfs(n, m, src).second; long long p2 = eachPushes(n, m, src); multi.push_back(p1); each.push_back(p2); ratio.push_back(p2 / p1); if (!CSV) printf(" %d × %d,a = %2d:多源 %6lld 次,每源各一次 %8lld 次,倍数 %lld\n", n, m, a, p1, p2, p2 / p1); } if (!CSV) printf(" ⇒ 顶格 n = m = 500、a = 10⁵:多源 250000 次," "每源各一次 %lld 次\n\n", 250000LL * 100000LL); row("pushMulti", multi); row("pushEach", each); row("pushRatio", ratio); row("worstEach", {250000LL * 100000LL}); }
/* ② 无障碍网格上:多源 BFS ≡ 到最近感染源的曼哈顿距离 */ { long long cells = 0, same = 0; for (int n = 1; n <= 12; n++) for (int m = 1; m <= 12; m++) for (int a = 1; a <= 4; a++) { mt19937 rng((unsigned)(n * 10000 + m * 100 + a)); vector<pair<int, int>> src; for (int i = 0; i < a; i++) src.push_back({(int)(rng() % (unsigned)n) + 1, (int)(rng() % (unsigned)m) + 1}); auto d = multiBfs(n, m, src).first; for (int x = 1; x <= n; x++) for (int y = 1; y <= m; y++) { int best = INT_MAX; for (auto [sx, sy] : src) best = min(best, abs(sx - x) + abs(sy - y)); cells++; if (d[x][y] == best) same++; } } if (!CSV) printf("② 无障碍网格上,多源 BFS ≡ min 曼哈顿距离:逐格比了 %lld 格,一致 %lld 格\n\n", cells, same); row("manhattanSame", {cells, same}); }
/* ③ ⚠ 那条等价的前提:挖一堵墙就不成立 */ { // . 1 . (1,1) 是感染源,'1' 是墙:(1,2) 和 (2,2) // . 1 . 想从 (1,1) 到 (1,3),只能绕最下面那一行 // . . . vector<string> wall = {".1.", ".1.", "..."}; vector<pair<int, int>> src = {{1, 1}}; auto d = multiBfs(3, 3, src, &wall).first; int bfs13 = d[1][3]; int man13 = abs(1 - 1) + abs(1 - 3); if (!CSV) printf("③ 挖一堵墙之后(3 × 3,墙在 (1,2) 和 (2,2),源在 (1,1)):\n" " 到 (1,3) 的真实步数 %d,曼哈顿距离只有 %d —— 那条等价立刻不成立\n\n", bfs13, man13); row("wallCase", {bfs13, man13}); }
/* ④ 「共用 dist」那个错法究竟算了什么 + 它的抓获率沿 a 走 */ { // (a) 它逐格等于「只有第一个感染源」 long long cells = 0, same = 0; for (int n = 1; n <= 10; n++) for (int m = 1; m <= 10; m++) for (int a = 1; a <= 5; a++) { mt19937 rng((unsigned)(n * 777 + m * 31 + a)); vector<pair<int, int>> src; for (int i = 0; i < a; i++) src.push_back({(int)(rng() % (unsigned)n) + 1, (int)(rng() % (unsigned)m) + 1}); auto bad = sharedBfs(n, m, src); auto only1 = multiBfs(n, m, {src[0]}).first; for (int x = 1; x <= n; x++) for (int y = 1; y <= m; y++) { cells++; if (bad[x][y] == only1[x][y]) same++; } } if (!CSV) printf("④ 「共用 dist」那版逐格等于「只有第一个感染源」:比了 %lld 格,一致 %lld 格\n", cells, same); row("sharedIsFirst", {cells, same});
// (b) 抓获率沿 a 走(300 轮,网格 8 × 8,每轮 6 个询问) vector<long long> caught; vector<int> as = {1, 2, 3, 5, 10}; if (!CSV) printf(" 它的抓获率沿「感染源个数 a」走(8 × 8,300 轮):\n"); for (int a : as) { int c = 0; for (int seed = 1; seed <= 300; seed++) { mt19937 rng((unsigned)seed * 131u + (unsigned)a); int n = 8, m = 8; vector<pair<int, int>> src; for (int i = 0; i < a; i++) src.push_back({(int)(rng() % (unsigned)n) + 1, (int)(rng() % (unsigned)m) + 1}); auto ok = multiBfs(n, m, src).first; auto bad = sharedBfs(n, m, src); bool differ = false; for (int i = 0; i < 6 && !differ; i++) { int x = (int)(rng() % (unsigned)n) + 1, y = (int)(rng() % (unsigned)m) + 1; if (ok[x][y] != bad[x][y]) differ = true; } if (differ) c++; } caught.push_back(c); if (!CSV) printf(" a = %2d:%3d / 300\n", a, c); } if (!CSV) printf(" ⇒ a = 1 那一档是**精确的 0** —— 它错在「第二个源」,而根本没有第二个源\n\n"); row("sharedCatch", caught); }
/* ⑤ 顶格那一档的读入 */ { // 第 1 行 "500 500 100000 100000\n",然后 a + b 行,每行最长 "500 500\n" = 8 字节 long long lines = 200000; long long bytes = (long long)strlen("500 500 100000 100000\n") + lines * 8; if (!CSV) printf("⑤ 顶格 a = b = 10⁵:读入 %lld 行坐标、%lld 个整数,最多 %lld 字节(%.2f MB)\n", lines, lines * 2, bytes, (double)bytes / 1048576.0); row("inputBytes", {lines, lines * 2, bytes}); } return 0;}点「运行 ▶」看结果
7一张总表
| 版本 | 复杂度 | 官方样例 | 结果 |
|---|---|---|---|
⚠ p1332Each |
O(a × nm),顶格 2.5 × 10¹⁰ |
✓ | ✗ TLE |
⚠⚠ p1332Shared |
O(nm) |
✗ 给 4 6 4 |
✗ WA(等于只有第一个源) |
p1332Manhattan |
O(ab),顶格 10¹⁰ |
✓ | ✗ TLE(但它是完美的对拍参照物) |
★ p1332 |
O(nm),和 a 无关 |
✓ | ★ AC |
顶格 a = b = 10⁵ ⇒ 读入 200000 行坐标、400000 个整数,最多 1600022 字节(1.53 MB)。
按第 10 章 P1104 那把尺子 —— 看的是字节数,不是数的个数 ——
1.53 MB 离第 6 章 P2367 那道「连 scanf 都不够」(两千万个整数)差着两个数量级。
⇒ scanf 绰绰有余。「要不要读入优化」还是那道三十秒的算术题。
- ★★ 多源 BFS 就是「第 0 层不止一个格子」。
想象一个连着所有源、边长为 0 的超级源点,BFS 的层序一点没变。
代价从
O(a × nm)掉到O(nm)—— 和源的个数完全无关。 - ★★★ 说清楚一个 bug「算了什么」,比说它「错了」有用得多。
那个「共用
dist」的写法逐格等于「只有第一个感染源」(15125 格一个不差)—— 知道了这句话,「a = 1时它是对的、对拍那一档必然是精确的 0」就是白送的推论。 - ★★★ 验算要走一条和算法完全无关的路 —— 但先问清楚那条路的前提。
无障碍网格上答案 = min 曼哈顿距离(24336 格一个不差),两条路一行代码都不共享;
⚠ 可挖一堵墙,同一个点就从 2 变成 6。
★ 而这条捷径不能当解法(
10¹⁰):参照物和解法本来就是两回事。