把迷宫字符画补一圈空白后,从最外圈多源 BFS 灌水:每个格心的字符距离折半就是到最近出口的步数,取最大值,一次 O(WH) 求出最坏点。
OJ: roj
题目 ID: 2036
难度:普及+/提高-
标签:BFSflood fill网格最短路usacopython
创建: 2026-10-01 04:33
更新: 2026-10-07 13:50
形式化题目
给定迷宫字符画:
- 空格:格子、格间通道、迷宫外的区域;
-、|:栅栏;+:栅栏柱子,只出现在偶数行与偶数列。
迷宫边界上恰有两段栅栏缺失(两个出口,按"格子 + 方向"计),且迷宫连通。 牛只能在上下左右方向移动,每移动到一个新的方格算一步,从出口迈出迷宫的那一步也算一步。
求
即从最"糟糕"的那一点出发、即使走最优路线也需要的最大步数。
样例(9。
正解
思路
第一步:朴素做法。 把迷宫读成"格子图":格子与上下左右四个邻居有边,当且仅当两者之间的 那个字符是空格;若这条边落在迷宫边界上,则这条边直接通向出口之外。然后对每个格子各做一次 单源 BFS,求出它到最近出口的最少步数,最后取最大值。
这个做法正确,但把同一份信息算了
第二步:倒过来看。 两个出口只是迷宫边界上的两个"孔",孔外面是同一片自由空间。
把整片自由空间当作超级源点(距离 0),让水从两个孔同时灌进迷宫:
每个格子在第几层被淹到,就是它到最近出口的步数。 一次多源 BFS 同时算出所有格子的答案,
复杂度回到
第三步:直接在字符画上灌水,而不是在格子图上。 把字符画的每个字符位当作节点, 上下左右相邻且目标字符是空格就连一条边。这样:
- 墙的判定消失了:
-、|、+都不是空格,水自然过不去; - 出口判定消失了:缺口本身是空格,水自然流得出去;
- 撞墙检测消失了:补边之后 BFS 永远不会越界。
第四步:补一圈空白。 把 ljust 到
第五步:把字符距离折半。 牛走一格 = 从格心跨过通道到相邻格心 = 2 个字符位;
从格心迈出迷宫也是 2 个字符位(格心 → 缺口 → 迷宫外的空格位)。反过来看,
空白位只有三种:格心(偶、偶)、格间中点(奇、偶 或 偶、奇)、最外圈
(奇、奇 一定是柱子 +)。按
所以只要对所有格心的距离取最大值再整除 2,就是题目要的答案。 整除这一步同时也把"源点距离是 0 而走出第一步算 1 步"的偏移一并消掉。
下面是样例补边后的样子:* 是最坏点 @ 是两段出口栅栏缺口。
0 1 2 3 4 5 6 7 8 9 0 1 2
0 | |
1 | +-+-+-+-+-+ |
2 | | *******| |
3 | +-+*+-+ +*+ |
4 | |*** | |*| |
5 | +*+-+-+ +*+ |
6 | |*| |*@*|
7 | +-+@+-+-+-+ |
8 | |可以看到第 0 行与第 8 行、第 0 列与第 12 列整圈都是新补的空白,两个 @ 正是迷宫的
两段出口栅栏缺口:一个在底边(格子
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 04:33
# update_at: 2026-10-01 04:45
import sys
from collections import deque
SPACE = ' ' # 空格这一个字符同时表示三种东西:格点、格间通道、栅栏之外
def build_grid(w: int, h: int, lines: list[str]) -> list[str]:
"""把 2*h+1 行的迷宫补成 (2*h+3) x (2*w+3),四周各包一圈空白。
有了这圈空白,所有空区域(通道 + 迷宫外的自由空间)直接连通;两段出口
栅栏是迷宫边缘上仅有的字符缺口,也就成了通道通向自由的唯一两个孔。
补行长度时多留一个字符,长度不足的输入行(USACO 原数据会去掉行尾空格)
也能被 ljust 补齐而不越界。
"""
width = 2 * w + 3
return ([' ' * width]
+ [' ' + line.ljust(width - 2) + ' ' for line in lines[:2 * h + 1]]
+ [' ' * width])
def flood_dist(grid: list[str]) -> list[int]:
"""从迷宫外做一次多源 BFS(洪水填充),返回每个字符位的距离。
距离按"字符位移次数"计:相邻两个字符位算 1 步。牛走一格 = 跨过 2 个字符位,
所以格子层数 = 该格子格心的字符距离 / 2,具体换算放在 solve 里做。
"""
width, height = len(grid[0]), len(grid)
dist = [-1] * (width * height)
queue = deque()
# 补出来的最外圈整体入队当超级源点:它们就是迷宫外的那片自由空间,距离为 0。
for v in range(width * height):
r, c = divmod(v, width)
if r in (0, height - 1) or c in (0, width - 1):
dist[v] = 0
queue.append(v)
while queue:
v = queue.popleft()
r, c = divmod(v, width)
for nr, nc in ((r - 1, c), (r + 1, c), (r, c - 1), (r, c + 1)):
if 0 <= nr < height and 0 <= nc < width and grid[nr][nc] == SPACE and dist[nr * width + nc] < 0:
dist[nr * width + nc] = dist[v] + 1
queue.append(nr * width + nc)
return dist
def solve() -> None:
data = sys.stdin.buffer.read().decode().splitlines()
w, h = map(int, data[0].split())
grid = build_grid(w, h, data[1:2 * h + 2])
width = 2 * w + 3
dist = flood_dist(grid)
# 格子 (r, c) 的格心在补齐网格里的位置是 (2r+2, 2c+2)。格心的字符距离恰好等于
# 该格子到最近出口的步数的两倍(每走一步正好跨 2 个字符位,且最短路会成对地
# 经历"通道中点 → 格心"),所以对所有格心取最大距离再整除 2 就是答案。
ans = max(dist[(2 * r + 2) * width + 2 * c + 2] for r in range(h) for c in range(w)) // 2
print(ans)
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-06 09:33
* update_at: 2026-10-06 09:33
*/
#include <iostream>
#include <string>
#include <queue>
using namespace std;
typedef long long ll;
const ll MAXR = 210; // 2*100+3 = 203,留一点余量
const ll MAXC = 82; // 2*38+3 = 79,留一点余量
ll w, h;
string grid[MAXR]; // grid[r][c] 是补齐后的字符画;最外圈是补出来的空白
ll dist[MAXR][MAXC]; // dist[r][c] 表示字符位 (r,c) 到迷宫外的最少字符步数,-1 表示未访问
ll total_rows; // 补齐后总行数 2*h+3
ll total_cols; // 补齐后总列数 2*w+3
// 从迷宫外(补出来的最外圈)做一次多源 BFS,只走空格,求每个字符位的字符距离。
void flood_fill() {
ll dr[4] = {-1, 1, 0, 0};
ll dc[4] = {0, 0, -1, 1};
queue<pair<ll, ll> > q;
for (ll r = 0; r < total_rows; r++) {
for (ll c = 0; c < total_cols; c++) {
dist[r][c] = -1;
if (r == 0 || r == total_rows - 1 || c == 0 || c == total_cols - 1) {
dist[r][c] = 0; // 最外圈就是迷宫外的自由空间,整体作为超级源点
q.push(make_pair(r, c));
}
}
}
while (!q.empty()) {
ll r = q.front().first;
ll c = q.front().second;
q.pop();
for (ll k = 0; k < 4; k++) {
ll nr = r + dr[k];
ll nc = c + dc[k];
if (nr < 0 || nr >= total_rows || nc < 0 || nc >= total_cols) continue;
if (grid[nr][nc] != ' ') continue; // 栅栏和柱子都不是空格,水自然过不去
if (dist[nr][nc] != -1) continue;
dist[nr][nc] = dist[r][c] + 1;
q.push(make_pair(nr, nc));
}
}
}
int main() {
cin >> w >> h;
total_rows = 2 * h + 3;
total_cols = 2 * w + 3;
string line;
size_t line_width = 2 * w + 1; // 原始字符画每行的字符数
getline(cin, line); // 吃掉读入 W H 后的换行
// 补齐四周各一圈空白:第 0 行与最后一行全空格,中间每行左右各加一个空格
grid[0] = string(total_cols, ' ');
for (ll r = 0; r < 2 * h + 1; r++) {
getline(cin, line);
if (!line.empty() && line[line.size() - 1] == '\r') line.erase(line.size() - 1);
while (line.size() < line_width) line.push_back(' '); // 原数据可能去掉行尾空格
grid[r + 1] = " " + line.substr(0, line_width) + " ";
}
grid[total_rows - 1] = string(total_cols, ' ');
flood_fill();
// 格子 (r,c) 的格心在补齐网格中的位置是 (2r+2, 2c+2)。牛每走一步跨 2 个字符位,
// 所以格心的字符距离恰好是该格子到最近出口步数的两倍,取最大距离整除 2 即为答案。
ll ans = 0;
for (ll r = 0; r < h; r++) {
for (ll c = 0; c < w; c++) {
ll steps = dist[2 * r + 2][2 * c + 2] / 2;
if (steps > ans) ans = steps;
}
}
cout << ans << endl;
return 0;
}复杂度
补边后网格有
- 时间复杂度:
,每个空白位入队一次、检查 4 个邻居。 对比逐格子单源 BFS 的 。 - 空间复杂度:
, grid约, dist至多 16037 个整数。
总结
把迷宫字符画补上一圈空白,让"迷宫外"成为网格里真实存在的一整片连通区域,
再把最外圈整体当作多源 BFS 的源点:每个格心的字符距离折半就是它到最近出口的步数,
取所有格子的最大值即为答案。这次转换去掉了墙判定、出口判定和越界检测三类边界代码,
并把逐格子单源 BFS 的
图示解析
每个格子的层数
下表给出样例中每个格子的 BFS 结果,单元格写作"字符距离 / 牛的步数"。 字符距离是对补边网格全局做一次多源 BFS 得到的;折半就是答案,加粗的是最大值。
| 方格行 \ 列 | c0 | c1 | c2 | c3 | c4 |
|---|---|---|---|---|---|
| r0 | 14 / 7 | 12 / 6 | 10 / 5 | 8 / 4 | 6 / 3 |
| r1 | 16 / 8 | 14 / 7 | 16 / 8 | 8 / 4 | 4 / 2 |
| r2 | 18 / 9 | 2 / 1 | 4 / 2 | 6 / 3 | 2 / 1 |
三个观察点:
- 所有字符距离都是偶数,所以折半不丢精度;
- 最小值 2 出现在两个出口所在的格子
与 上,对应"从缺口迈出迷宫算 1 步"; - 最大值 18 在左下角
,折半得 9,与样例输出一致。
模型转换路线
flowchart TD
A["字符画 2H+1 x 2W+1<br/>墙与柱子不是空格"] --> B["四周补一圈空白<br/>补成 2H+3 x 2W+3"]
B --> C["最外圈全部入队<br/>距离 0,两个缺口就是两个孔"]
C --> D["在字符位上多源 BFS<br/>只走空格,一次 O(WH)"]
D --> E["取所有格心的最大距离<br/>除以 2 就是答案"]
整条路线的关键是第二步:把"迷宫外"变成网格里真实存在的一圈节点,出口就不需要任何特殊判定; 第四步则保证一次 BFS 同时解决所有格子的询问,把平方复杂度降回线性。