0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1141,日期见页头。两边不一致时信原站。
题目描述
有一个仅由数字 0 与 1 组成的 n × n 格迷宫。
若你位于一格 0 上,那么你可以移动到相邻 4 格中的某一格 1 上;
同样若你位于一格 1 上,那么你可以移动到相邻 4 格中的某一格 0 上。
你的任务是:对于给定的迷宫,询问从某一格开始能移动到多少个格子(包含自身)。
输入格式
第一行为两个正整数 n, m。
下面 n 行,每行 n 个字符,字符只可能是 0 或者 1,字符之间没有空格。
接下来 m 行,每行两个用空格分隔的正整数 i, j,对应了迷宫中第 i 行第 j 列的一个格子,
询问从这一格开始能移动到多少格。
输出格式
m 行,对于每个询问输出相应答案。
说明/提示
对于样例,所有格子互相可达。
- 对于 20% 的数据,
n ≤ 10; - 对于 40% 的数据,
n ≤ 50; - ★ 对于 50% 的数据,
m ≤ 5; - 对于 60% 的数据,
n, m ≤ 100; - 对于 100% 的数据,
1 ≤ n ≤ 1000,1 ≤ m ≤ 10⁵。
输入输出样例
输入
2 2 01 10 1 1 2 2
输出
4 4
01 / 10:每一格的四个邻居里,数字都和自己不同 ⇒ 四个格子连成一块,答案都是 4。
⚠ 注意上面那五档数据范围里,有一档是按 m 分的(50% 的数据 m ≤ 5)——
第 ② 步会说这一句值多少分。
1★ 第一版:照题面一个字一个字翻译 —— 每次询问跑一遍泛洪
// ⚠ P1141 第一版:**每次询问从那一格跑一遍 BFS**,数一数走过几个格子。//// 这是照着题面一个字一个字翻译出来的:「询问从某一格开始能移动到多少个格子」——// 那就从那一格开始走一遍。**答案永远是对的**,对拍跑多少轮都抓不到它。//// 复杂度 O(mn²)。顶格 n = 1000、m = 10⁵ ⇒ **10¹¹**,一秒钟连零头都跑不完。//// ★★ 但**别急着删它** —— 题面那五档数据范围里,有一档是**按 m 分的**:// 「对于 **50%** 的数据,**m ≤ 5**」// 那一档它只要 5 × 10⁶ 次,**哪怕 n 顶格也稳稳拿 50 分**。// ⇒ ★★★ 出题人等于直接告诉你:**这个暴力的瓶颈是 m,不是 n。**//// ★ 它同时是本页的对拍参照物(和「一次染色」那条路一行代码都不共享)。//// ⚠ 命令行给一个 `count` 参数,它就只打「一共走过多少个格子」。
#include <bits/stdc++.h>using namespace std;
int main(int argc, char** argv) { bool countOnly = (argc > 1 && string(argv[1]) == "count"); ios::sync_with_stdio(false); cin.tie(nullptr);
int n, m; if (!(cin >> n >> m)) return 0; vector<string> g(n); for (int i = 0; i < n; i++) cin >> g[i];
const int dx[4] = {-1, 1, 0, 0}, dy[4] = {0, 0, -1, 1}; string out; long long steps = 0; vector<pair<int, int>> st; vector<vector<char>> vis(n, vector<char>(n, 0)); for (int t = 0; t < m; t++) { int qi, qj; cin >> qi >> qj; --qi; --qj; // 题面是 1-based for (int i = 0; i < n; i++) fill(vis[i].begin(), vis[i].end(), 0); int cnt = 0; st.clear(); st.push_back({qi, qj}); vis[qi][qj] = 1; while (!st.empty()) { auto [x, y] = st.back(); st.pop_back(); cnt++; for (int d = 0; d < 4; d++) { int nx = x + dx[d], ny = y + dy[d]; if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue; steps++; if (g[nx][ny] == g[x][y] || vis[nx][ny]) continue; // ★ 只能走到「不同」的格子 vis[nx][ny] = 1; st.push_back({nx, ny}); } } out += to_string(cnt); out += '\n'; } if (countOnly) { printf("%lld\n", steps); return 0; } fwrite(out.data(), 1, out.size(), stdout); return 0;}点「运行 ▶」看结果
答案永远是对的 —— 对拍跑多少轮都抓不到它。坏掉的只有复杂度:
顶格 n = 1000、m = 10⁵ ⇒ 10¹¹ 次。
「对于 50% 的数据,
m ≤ 5。」
注意这一档没有限制 n。也就是说:n 顶格 1000、只问 5 次 ⇒ 暴力只要 5 × 10⁶ 次。
实测那一档(n = 1000, m = 5)它跑 0.00 秒。
⇒ ★★ 出题人等于直接把「这个暴力的瓶颈是 m,不是 n」写在题面上了 ——
而这一档值 50 分。⇒ 想不出正解也必须先把它交上去。
★ 本轮那条主线的又一次现场:第 24 章 P1776、第 23 章 P1060、
上一轮 P3916 都量过 ——数据范围那几行不是背景,每一行都是一件工具。
⚠ 而这一档特别值得留意:它是唯一一档按 m 分的,混在四档「按 n 分」里面,很容易看漏。
2⚠ 第二版:按格子记忆化 —— 改进了,可没改够(实测只省 2%)
「同一格问过就别再问」听着很对。可最坏情况下每次询问都是一个新格子 ——
顶格 m = 10⁵ 而格子有 10⁶ 个,压根撞不上。
顶格随机数据(n = 1000, m = 10⁵) |
|
|---|---|
| 10⁵ 个询问里真正跑了几次 BFS | ★ 95 191 |
| 端到端耗时:不记忆化 / 记忆化 | 2.06 秒 / 2.02 秒 —— ★ 省了 2% |
一个连通块里可能有几十万个格子,它们的答案完全一样。 按「格子」记忆化,粒度差了整整一个数量级 ⇒ 省下的只是零头。
⇒ 下一版把粒度换成「块」,复杂度当场从 O(mn²) 掉到 O(n² + m)。
3★★ 正解:一次扫描给每个连通块编号 + 记住它有多大,询问 O(1)
// ★★ P1141 正解:**一次扫描,给每个连通块编号 + 记住它有多大**,询问 O(1)。//// ★★★ 凭什么能这么干 —— 这一步不证清楚就只是碰运气:// 「从 A 走得到 B」这件事在这张图上是**对称的**(相邻且数字不同 ⇒ 两边互相走得通),// 于是「互相可达」是一个**等价关系** ⇒ 格子被划分成若干**连通块**,// 而**同一个块里每个格子的答案都等于这个块的大小**。// ⚠ 这一条离开对称性就塌了 —— [上一轮 P3916](/sol/p3916/) 那张**有向图**上,// 「v 能到谁」和「谁能到 v」是两个不同的问题,压根没有「块大小」这回事。//// for (每个还没编号的格子) { 泛洪一次,把整块染成同一个编号,顺便数出块的大小 }// 答案(i, j) = size[ id[i][j] ]//// 复杂度 O(n² + m):顶格 10⁶ + 10⁵。⇒ 比「每次询问跑一遍」快 **10⁵ 倍**。//// ⚠ DFS 写成**迭代**的:顶格 n = 1000 ⇒ 一个块可能有 10⁶ 个格子,// 递归到底会爆栈([B3625](/sol/b3625/) 第 ⑥ 步量过门槛:本机 17.4 万层)。见 p1141Rec.cpp。// ⚠ 答案上界 n² = 10⁶ ⇒ `int` 绰绰有余。
#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<string> g(n); for (int i = 0; i < n; i++) cin >> g[i];
const int dx[4] = {-1, 1, 0, 0}, dy[4] = {0, 0, -1, 1}; vector<vector<int>> id(n, vector<int>(n, -1)); vector<int> sz; vector<pair<int, int>> st;
for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) { if (id[i][j] >= 0) continue; int cur = (int)sz.size(), cnt = 0; st.clear(); st.push_back({i, j}); id[i][j] = cur; while (!st.empty()) { auto [x, y] = st.back(); st.pop_back(); cnt++; for (int d = 0; d < 4; d++) { int nx = x + dx[d], ny = y + dy[d]; if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue; if (g[nx][ny] == g[x][y] || id[nx][ny] >= 0) continue; id[nx][ny] = cur; st.push_back({nx, ny}); } } sz.push_back(cnt); // ★ 整块共用这一个数 }
string out; for (int t = 0; t < m; t++) { int qi, qj; cin >> qi >> qj; out += to_string(sz[id[qi - 1][qj - 1]]); // ★ 题面是 1-based out += '\n'; } fwrite(out.data(), 1, out.size(), stdout); return 0;}点「运行 ▶」看结果
「从 A 走得到 B」在这张图上是对称的: 「相邻且数字不同」这个条件对 A、B 完全对等 ⇒ A 能一步到 B,B 就能一步回 A。
⇒ 「互相可达」是一个等价关系 ⇒ 格子被划分成若干连通块, 而同一个块里每个格子的答案都等于这个块的大小。
⚠⚠ 这一条离开对称性就塌了。 上一轮 P3916 那张有向图上,
「v 能到谁」和「谁能到 v」是两个不同的问题,压根没有「块大小」这回事 ——
那道题只好反向建图、从大到小扫。
⇒ ★★ 「同一块内答案相同」不是网格题的通性,是这道题的边对称带来的。
★ 而这句话验过,没有只停在纸上:逐格拿「从这一格泛洪数出来的格子数」和「块大小」比, 8 311 个格子里对不上 0 个(300 轮,两条路一行代码都不共享)。
4⚠ 两个错法,官方样例都一测就死
// ✗ P1141 错法一:把「只能走到**不同**的数字」写成了「走到**相同**的数字」。//// if (g[nx][ny] == g[x][y]) continue; // 正解:相同就不能走// if (g[nx][ny] != g[x][y]) continue; // ★ 这一版:不同就不能走//// 一个字符之差。⇒ 它算的是**同色连通块**的大小 —— 一个完全不同、但同样有意义的量。// ★ 说清楚它算了什么之后,「什么时候它会蒙对」就是白送的:// `1 × 1` 的迷宫(只有自己)两边都是 1;棋盘格(黑白相间)上它恒等于 1。
#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<string> g(n); for (int i = 0; i < n; i++) cin >> g[i];
const int dx[4] = {-1, 1, 0, 0}, dy[4] = {0, 0, -1, 1}; vector<vector<int>> id(n, vector<int>(n, -1)); vector<int> sz; vector<pair<int, int>> st; for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) { if (id[i][j] >= 0) continue; int cur = (int)sz.size(), cnt = 0; st.clear(); st.push_back({i, j}); id[i][j] = cur; while (!st.empty()) { auto [x, y] = st.back(); st.pop_back(); cnt++; for (int d = 0; d < 4; d++) { int nx = x + dx[d], ny = y + dy[d]; if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue; if (g[nx][ny] != g[x][y] || id[nx][ny] >= 0) continue; // ★ 反了 id[nx][ny] = cur; st.push_back({nx, ny}); } } sz.push_back(cnt); } string out; for (int t = 0; t < m; t++) { int qi, qj; cin >> qi >> qj; out += to_string(sz[id[qi - 1][qj - 1]]); out += '\n'; } fwrite(out.data(), 1, out.size(), stdout); return 0;}点「运行 ▶」看结果
一个字符之差(== ↔ !=)。★ 说清楚它算了什么:它求的是同色连通块的大小 ——
一个完全不同、但同样有意义的量。对拍 300 轮被抓 280 次。
⇒ 而「什么时候它会蒙对」就是白送的:1 × 1 的迷宫两边都是 1,棋盘格上它恒等于 1。
题面是 1-based、数组是 0-based。这一条和算法一点关系都没有,可它一分不给 —— 而且它的表现很像「算法错了」:打出来全是别的格子的答案,看着完全合理。
5★★★ 这一页最锋利的一条:同样是顶格,随机数据和棋盘格差 1464 倍
把染色那一步写成递归 DFS(别的一个字不改):
顶格 n = 1000 |
随机 01 方阵 | ★ 棋盘格(0101… 相间) |
|---|---|---|
| 最大的连通块 | 683 格(共 131 641 块) | ★ 1 000 000 格(1 块) |
| ⇒ 差 | ★ 1464 倍 | |
| 递归染色最深到第几层 | ★ 172 | ★ 段错误(退出码 139) |
| 「每询问一次 BFS」那版 | 2.06 秒 | ★ 120 秒还没完 |
| 「按格子记忆化」那版 | 2.02 秒 | ★ 60 秒还没完 |
| 正解 | 0.04 秒 | 0.02 秒 |
⇒ ★★★ 顺手造一组「顶格随机」数据跑一遍,这一页三个坑一个都看不见: 递归版跑得好好的(172 层,余量一千倍),暴力也只慢 2 倍 (看着像「常数优化一下就能过」)。只有造对形状,它们才现形。
★ 而棋盘格为什么是最坏形状,一句话就能说清: 相邻两格必然不同 ⇒ 每条边都能走 ⇒ 整张图连成一块。 ⇒ 这是第 51 章那条「造一组大数据跑一次也不够,要造对形状」的又一次现场, 也是上一轮 P3916(顶格随机 0.23 秒 / 顶格一条链 36.35 秒)的姊妹页。
同题单 B3625 第 ⑥ 步在同一台机器上量出:这个量级的 dfs
门槛是 17.4 万层 / 每层约 48 字节(A 机 · WSL2 · ulimit -s = 8192 KB · 2026-08-30)。
| 顶格最深要几层 | 对 17.4 万的比 | 结论 | |
|---|---|---|---|
B3625(100 × 100) |
10 000 | 0.06 倍 | ★ 安全,余量 17.4 倍 |
★ 这道题(1000 × 1000 棋盘格) |
10⁶ | ★ 5.7 倍 | ★★ 必炸 |
⇒ 同一张题单里两道网格搜索题,一道随便递归,一道想都别想 —— 第 29 章 P5318 那条公式(门槛 = 栈上限 ÷ 每层字节数)在这儿连着用了两次。
6★ 顺带:随机 01 方阵里的块,比你以为的小得多
| 随机 01 方阵(每档 30 轮) | n = 8 |
n = 40 |
n = 200 |
|---|---|---|---|
| 最大块平均 | 22 | 136 | 366 |
| 最大块的最大值 | 38 | 285 | 682 |
| 而棋盘格是(整张图一块) | 64 | 1 600 | 40 000 |
⇒ ★ 格子数涨了 625 倍,随机方阵里最大的块只涨了 17 倍。 随机 01 序列里,「相邻不同」这件事平均每两格就断一次,块自然长不大。 ⚠ 这正是上一步那张表的来处 —— 别拿「顶格随机」当「顶格」。
7★ 对拍这一页
300 轮(n 随机 2~8,参照物 = 每询问一次泛洪) |
|
|---|---|
| 正解(一次染色)≡ 暴力 | ★ 不一致 0 轮 |
| 「不同」写成「相同」 | 280 |
| 询问下标忘了减一 | 越界 / 恒错(官方样例一测就死) |
| ★ 逐格验「块大小 ≡ 从这一格泛洪的格子数」 | ★ 8 311 格里 0 个对不上 |
8度量程序和生成器
9一页纸
| ★★ 关键的一步 | 边是对称的 ⇒ 互相可达是等价关系 ⇒ 一次染色 + 块大小,询问 O(1) |
| ★ 那一步验过 | 逐格「泛洪数出来的格子数 ≡ 块大小」,8 311 格 0 个对不上 |
| ★★★ 题面那一档 | 「50% 的数据 m ≤ 5」—— 五档里唯一按 m 分的,暴力稳拿 50 分 |
| ⚠ 第二版 | 按格子记忆化只省 2%(10⁵ 个询问里 95 191 个还是新格子)——粒度错了 |
| ★★★ 顶格 ≠ 最坏 | 随机方阵最大块 683、递归最深 172 层;棋盘格 10⁶ 格、段错误。差 1464 倍 |
| ★★ 递归能不能用 | 门槛 17.4 万层 ⇒ B3625 的 10⁴ 层安全、这道题的 10⁶ 层必炸 |
| 两个错法 | 「不同」写成「相同」⇒ 算的是同色块(280/300);下标忘减一 ⇒ 越界 |