题单 · 习题解析

洛谷 P1141 01迷宫

★★ 边是对称的 ⇒ 互相可达是等价关系 ⇒ 一次染色 + 块大小(逐格验过:8311 格 0 个对不上);★★★ 题面五档里**唯一按 m 分的**那档(m ≤ 5)让暴力稳拿 50 分;★★★ 顶格 ≠ 最坏:随机方阵最大块 683 格 / 递归最深 172 层,棋盘格 10⁶ 格 / 段错误,差 1464 倍

原题:洛谷 P1141出自 第 30 章 图上的 DFS 与 BFS、连通性 的题单题面本地存档:2026-08-30
⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目描述

有一个仅由数字 01 组成的 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 ≤ 10001 ≤ m ≤ 10⁵

输入输出样例

输入

2 2
01
10
1 1
2 2

输出

4
4

01 / 10:每一格的四个邻居里,数字都和自己不同 ⇒ 四个格子连成一块,答案都是 4

⚠ 注意上面那五档数据范围里,有一档是按 m 分的(50% 的数据 m ≤ 5)—— 第 ② 步会说这一句值多少分。

1★ 第一版:照题面一个字一个字翻译 —— 每次询问跑一遍泛洪

p1141Naive.cpp⚠ 每询问一次 BFS:O(mn²)
// ⚠ 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

答案永远是对的 —— 对拍跑多少轮都抓不到它。坏掉的只有复杂度: 顶格 n = 1000m = 10⁵10¹¹ 次。

★★★ 题面那五档数据范围里,有一档是按 m 分的 —— 它直接告诉你瓶颈在哪

「对于 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%)

p1141Memo.cpp⚠ 同一格问过就别再问

「同一格问过就别再问」听着很对。可最坏情况下每次询问都是一个新格子 —— 顶格 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.cpp★★ 一次染色,O(n² + m)
// ★★ 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★★ 凭什么能这么干 —— 这一步不证清楚就只是碰运气

「从 A 走得到 B」在这张图上是对称的: 「相邻且数字不同」这个条件对 A、B 完全对等 ⇒ A 能一步到 B,B 就能一步回 A。

⇒ 「互相可达」是一个等价关系 ⇒ 格子被划分成若干连通块, 而同一个块里每个格子的答案都等于这个块的大小

⚠⚠ 这一条离开对称性就塌了。 上一轮 P3916 那张有向图上, 「v 能到谁」和「谁能到 v」是两个不同的问题,压根没有「块大小」这回事 —— 那道题只好反向建图、从大到小扫。 ⇒ ★★ 「同一块内答案相同」不是网格题的通性,是这道题的边对称带来的。

★ 而这句话验过,没有只停在纸上:逐格拿「从这一格泛洪数出来的格子数」和「块大小」比, 8 311 个格子里对不上 0 个(300 轮,两条路一行代码都不共享)。

4⚠ 两个错法,官方样例都一测就死

p1141Same.cpp✗ 「不同」写成「相同」(样例打 1)
// ✗ 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

一个字符之差(==!=)。★ 说清楚它算了什么:它求的是同色连通块的大小 —— 一个完全不同、但同样有意义的量。对拍 300 轮被抓 280 次。 ⇒ 而「什么时候它会蒙对」就是白送的:1 × 1 的迷宫两边都是 1,棋盘格上它恒等于 1。

p1141Base.cpp✗ 询问下标忘了减一

题面是 1-based、数组是 0-based。这一条和算法一点关系都没有,可它一分不给 —— 而且它的表现很像「算法错了」:打出来全是别的格子的答案,看着完全合理。

5★★★ 这一页最锋利的一条:同样是顶格,随机数据和棋盘格差 1464 倍

把染色那一步写成递归 DFS(别的一个字不改):

p1141Rec.cpp⚠ 递归染色 —— 顶格棋盘格直接段错误
★★★ 顶格随机数据下它毫发无损 —— 递归最深只有 172 层
顶格 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度量程序和生成器

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

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);下标忘减一 ⇒ 越界