B - Blind Alley - 题解
标签与难度
标签: 图论, 广度优先搜索 (BFS), 动态规划, 预处理, 网格图 难度: 1900
题目大意喵~
各位铲屎官...啊不,各位玩家好喵~!我们正在设计一个超级马里奥关卡,它是一个 的网格。我们要控制可爱的气球马里奥,从左上角的 出发,目标是到达第一行的最右边 。
地图上有些格子是障碍物(用 '1' 表示),马里奥不能进入。马里奥每次可以向上、向下或向右移动一格,但不能移出边界或进入障碍物。
这个游戏的特别之处在于,马里奥的视野是有限的!当马里奥在 时,他只能看到从第 列到第 列的所有格子。超出这个范围的地图对他来说是未知的,喵~
现在的问题是:这个关卡是否存在一个“死胡同”陷阱?一个陷阱需要满足以下三个条件:
- 存在一条从起点 到某个点 的路径 。
- 在这条路径 上的每一步,从 移动到 时,根据在 的视野,玩家都无法排除从 到达终点 的可能性。也就是说,玩家每一步都觉得“嗯,还有希望!”,然后走了进去。
- 然而,当马里奥真正到达 后,他发现,实际上已经无法从这里到达终点 了。呜,被骗进死胡同了!
我们需要判断这样的陷阱是否存在。如果存在,输出 "Yes";否则输出 "No",呐。
解题思路分析
这道题读起来有点绕,特别是那个“无法排除可能性”的条件,对吧?别担心,让我来帮你理清思路,喵~
问题的核心在于,玩家的决策是基于有限的、不完整的信息(视野内的地图),而最终的结果是基于完整的、真实的地图。一个“盲巷”陷阱,就是利用这种信息差来欺骗玩家。
我们可以把问题分解成两个主要部分来思考:
- 了解地图的“真相”:对于地图上的任意一个点,它到底能不能到达终点呢?
- 模拟玩家的“探索”:从起点出发,根据玩家的视野和对“真相”的判断,他会不会走进一个看似有希望,实则无路可走的陷阱里。
第一步:揭示地图的全部真相!
为了判断玩家会不会被骗,我们首先得像开了全图挂一样,知道从每个格子出发,最远能向右走到哪一列。我们可以用一个二维数组 max_reach[r][c] 来存储这个“真相”:从格子 出发,能到达的最远列号是多少。如果能到达终点,那么这个值就是 。
怎么计算这个 max_reach 数组呢?这就像一个动态规划(DP)问题!我们可以从右往左,一列一列地计算。
- 基础情况:对于最右边的一列(第 列),任何一个不是障碍的格子
(r, M),它的max_reach[r][M]自然就是 啦。 - 递推关系:当我们计算第 列的
max_reach[r][c]时,马里奥可以:- 向右移动到
(r, c+1)。所以max_reach[r][c]至少能达到max_reach[r][c+1](如果(r, c+1)不是障碍)。 - 在第 列内部上下移动。这意味着,在第 列里,所有连通(可以互相到达)的空格子,它们的
max_reach值应该是共享的,都等于这个连通块里所有格子的最大max_reach值。
- 向右移动到
所以,我们的计算步骤是(从 c = M-1 到 1):
- 向右看:对于第 列的每个格子
(r, c),如果它和(r, c+1)都不是障碍,那么先把max_reach[r][c]的值设为max_reach[r][c+1]。 - 上下看:在第 列内部,信息需要传播。我们可以先从上到下扫一遍,用上一行的信息更新当前行 (
max_reach[r][c] = max(max_reach[r][c], max_reach[r-1][c]));再从下到上扫一遍,用下一行的信息更新当前行 (max_reach[r][c] = max(max_reach[r][c], max_reach[r+1][c]))。这样一上一下,就能保证同一连通块内的信息都同步到最大值了,喵~
做完这个预处理,我们就得到了一个“真相地图” max_reach!
第二步:寻找那个致命的陷阱!
现在我们有了 max_reach 数组,可以开始模拟玩家的旅程了。我们可以从起点 (1, 1) 进行一次广度优先搜索(BFS),来探索所有玩家可能走到的地方。
在BFS的每一步,比如从当前格子 curr = (r, c) 移动到邻居格子 next = (nr, nc),我们需要判断这个移动是否会把玩家引入一个盲巷陷阱。
一个盲巷陷阱被触发,需要同时满足两个条件:
- 这是一个真·陷阱 (The Trap):玩家移动到的
next格子,实际上是无法到达终点的。用我们的真相地图来说,就是max_reach[nr][nc] < M。 - 这是一个有欺骗性的陷阱 (The Deception):在玩家做出移动决定时,也就是在
curr = (r, c)这个位置,他并不知道next是个陷阱。为什么不知道呢?因为导致陷阱的障碍物,在他的视野之外!- 玩家在
(r, c),他的视野范围是[c, min(c+K, M)]。 next格子最终被困住,是因为它最远只能走到max_reach[nr][nc]这一列。- 如果这个“终点”
max_reach[nr][nc]在玩家的视野之外,玩家就会乐观地认为“嘿,我能走到一个我看不见的地方,那后面肯定路是通的吧!”,然后就上当了。 - 视野外的列是从
c+K开始的(如果c+K <= M)。所以,欺骗条件就是max_reach[nr][nc] >= c + K。
- 玩家在
总结一下:我们在从 (1,1) 开始的BFS中,每当要从 (r, c) 移动到 (nr, nc) 时,就检查下面这个条件:
如果这个条件成立,恭喜!我们找到了一个盲巷陷阱,可以直接输出 "Yes" 并结束程序。
如果整个BFS都结束了,还没有找到任何满足上述条件的移动,那就说明这个关卡设计得很“诚实”,没有陷阱。我们就输出 "No"。
这个两步走的策略是不是很清晰了呢,喵~?
代码实现
#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <algorithm>
using namespace std;
// 定义移动方向:上、下、右
const int dr[] = {-1, 1, 0, 0};
const int dc[] = {0, 0, 1, -1}; // 右移是(0,1), 左移是(0,-1)
void solve() {
int n, m, k;
cin >> n >> m >> k;
vector<string> grid(n);
for (int i = 0; i < n; ++i) {
cin >> grid[i];
}
// --- 步骤 1: 预处理 max_reach 数组 ---
// max_reach[r][c] 表示从 (r, c) 出发能到达的最远列号(0-indexed)
vector<vector<int>> max_reach(n, vector<int>(m, -1));
// 从右往左逐列计算
for (int c = m - 1; c >= 0; --c) {
// 首先,根据右边一列的信息进行初始化
for (int r = 0; r < n; ++r) {
if (grid[r][c] == '0') {
if (c == m - 1) {
max_reach[r][c] = m - 1;
} else if (grid[r][c+1] == '0') {
max_reach[r][c] = max_reach[r][c+1];
} else {
max_reach[r][c] = c;
}
}
}
// 然后,在当前列内通过上下移动传播信息
// 传播两次(一次向下,一次向上)确保信息在连通块内充分流动
for (int iter = 0; iter < 2; ++iter) {
// 从上到下
for (int r = 1; r < n; ++r) {
if (grid[r][c] == '0' && grid[r-1][c] == '0') {
max_reach[r][c] = max(max_reach[r][c], max_reach[r-1][c]);
}
}
// 从下到上
for (int r = n - 2; r >= 0; --r) {
if (grid[r][c] == '0' && grid[r+1][c] == '0') {
max_reach[r][c] = max(max_reach[r][c], max_reach[r+1][c]);
}
}
}
}
// --- 步骤 2: BFS 搜索盲巷陷阱 ---
queue<pair<int, int>> q;
vector<vector<bool>> visited(n, vector<bool>(m, false));
if (grid[0][0] == '0') {
q.push({0, 0});
visited[0][0] = true;
}
bool found_alley = false;
while (!q.empty()) {
pair<int, int> curr = q.front();
q.pop();
int r = curr.first;
int c = curr.second;
// 探索上、下、右三个方向
for (int i = 0; i < 3; ++i) {
int nr = r + dr[i];
int nc = c + dc[i];
// 检查新位置是否合法
if (nr >= 0 && nr < n && nc >= 0 && nc < m && grid[nr][nc] == '0') {
// 检查是否触发盲巷陷阱条件
// 1. next是陷阱: max_reach[nr][nc] < m - 1
// 2. 陷阱有欺骗性: max_reach[nr][nc] >= c + k (从c看不到陷阱的尽头)
if (max_reach[nr][nc] < m - 1 && max_reach[nr][nc] >= c + k) {
found_alley = true;
break;
}
// 如果不是陷阱,且未访问过,则加入队列继续探索
if (!visited[nr][nc]) {
visited[nr][nc] = true;
q.push({nr, nc});
}
}
}
if (found_alley) {
break;
}
}
if (found_alley) {
cout << "Yes\n";
} else {
cout << "No\n";
}
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
solve();
}
return 0;
}复杂度分析
时间复杂度:
- 对于每个测试用例,预处理
max_reach数组时,我们遍历了整个网格。每个格子被访问常数次(一次来自右侧,几次来自上下传播),所以这部分的复杂度是 。 - 接下来的BFS搜索最多也只会访问每个格子一次,所以复杂度也是 。
- 因此,总的时间复杂度是 。由于有 组测试数据,总时间复杂度为 ,符合题目限制。
- 对于每个测试用例,预处理
空间复杂度:
- 我们需要空间来存储地图
grid、max_reach数组和BFS用的visited数组。它们的大小都和网格大小成正比,所以空间复杂度是 。
- 我们需要空间来存储地图
知识点总结
这道题是一个非常有趣的图论问题,它将经典的寻路算法与信息不对称的概念结合了起来,喵~
- 预处理与动态规划: 解决这类问题的一个常用技巧是,将一些可以在后续查询中重复使用的“全局”信息预先计算出来。本题中的
max_reach数组就是一个例子,它通过类似动态规划的方式,从后向前推导,为我们提供了每个点的“真实命运”。 - 广度优先搜索 (BFS): BFS 是在图上寻找最短路径或进行可达性分析的利器。在这里,我们用它来模拟玩家的探索过程,一步步地扩展可到达的区域。
- 问题建模: 最关键的一步是将题目中复杂的文字描述,特别是关于“玩家感知”的部分,转化为清晰、可计算的数学或逻辑条件。
max_reach[nr][nc] < M - 1和max_reach[nr][nc] >= c + k就是我们对“陷阱”和“欺骗”这两个概念的精确建模。
希望这篇题解能帮到你,祝你刷题愉快,早日成为算法大师喵!🐾