图论专题(十五):BFS的“状态升维”——带着“破壁锤”闯迷宫 - 详解
哈喽各位,我是前端小L。
欢迎来到我们的图论专题第十五篇!我们已经习惯了在迷宫里绕着墙走。但今天,我们获得了一种超能力:我们可以消除最多 k 个障碍物。
这道题是 BFS 算法进阶的里程碑。它教导我们:当移动的代价不仅仅是“步数”,还包含“资源消耗”时,我们的每一个“状态”,都必须携带这个“资源”的信息。
力扣 1293. 网格中的最短路径
https://leetcode.cn/problems/shortest-path-in-a-grid-with-obstacles-elimination/

题目分析:
输入:
m x n的网格grid,整数k。规则:
移动:上下左右。
障碍:
1可以被消除,但最多消除k个。空地:
0可以自由通过。
目标:从
(0, 0)到(m-1, n-1)的最短路径(步数)。
为什么传统的二维 BFS 会失效?
在传统的 BFS 中,我们用 visited[r][c] 来记录是否访问过。如果访问过,就不再入队。这是基于一个假设:第一次到达某个格子,一定是状态最好的(步数最少)。
但是在这里,到达同一个格子 (r, c),可能有两种情况:
路径 A:绕了一大圈到了
(r, c),没消除任何障碍(剩余消除次数k)。路径 B:走了直线到了
(r, c),但消除了一堵墙(剩余消除次数k-1)。
路径 B 虽然步数少,但它消耗了资源;路径 A 哪怕步数多,但它保留了资源。唯一的希望!就是如果后面的路还有障碍,路径 A 可能才 简单的 visited[r][c] 无法区分这两种情况,可能会错误地把更有潜力的路径 A 给“剪枝”掉。
“Aha!”时刻:状态升维 (State Expansion)
为了区分上述情况,我们需要在状态中加入“剩余消除次数”。 我们的节点不再是 (r, c),而是 (r, c, remain_k)。
visited 数组的进化:
方案一(三维数组):
visited[r][c][remain_k]。表示“以剩余remain_k次机会到达(r, c)”是否发生过。方案二(二维数组存最优值 - 推荐):
visited[r][c]存储到达该点时历史上最大的剩余消除次数。如果我们再次来到
(r, c),且当前的remain_k小于等于 之前记录的visited[r][c],那这次探索毫无意义(步数没少,资源还更少),直接剪枝。如果当前的
remain_k大于visited[r][c],说明我们找到了一条“更富有”的路,值得继续探索,更新visited[r][c]并入队。
贪心剪枝(针对本题的特殊优化)
如果 k 非常大,大到足以消除起点到终点曼哈顿距离上的所有障碍,那我们根本不需要绕路,直接走曼哈顿距离(直线)就是最短的。
曼哈顿距离
dist = (m - 1) + (n - 1)。如果
k >= dist,直接返回dist。
代码达成 (BFS + 状态记录)
C++
#include
#include
using namespace std;
class Solution {
public:int shortestPath(vector>& grid, int k) {int m = grid.size();int n = grid[0].size();// 1. 贪心剪枝:如果 k 足够大,直接走曼哈顿距离if (k >= m + n - 2) {return m + n - 2;}// BFS 队列:存储 {row, col, remain_k}// 步数可以通过 BFS 的层数来统计queue> q;q.push({0, 0, k});// 状态记录:visited[r][c] 记录到达 (r, c) 时剩余的最大消除次数// 初始化为 -1vector> visited(m, vector(n, -1));visited[0][0] = k;int steps = 0;int dr[] = {0, 0, 1, -1};int dc[] = {1, -1, 0, 0};// 2. 启动 BFSwhile (!q.empty()) {int size = q.size();// 遍历当前层for (int i = 0; i < size; ++i) {vector curr = q.front();q.pop();int r = curr[0];int c = curr[1];int cur_k = curr[2];// 到达终点if (r == m - 1 && c == n - 1) {return steps;}// 探索邻居for (int dir = 0; dir < 4; ++dir) {int nr = r + dr[dir];int nc = c + dc[dir];if (nr >= 0 && nr < m && nc >= 0 && nc < n) {// 计算到达邻居需要的消除次数// 如果是空地(0),新k = cur_k// 如果是障碍(1),新k = cur_k - 1int next_k = cur_k - grid[nr][nc];// 核心判断:// 1. 剩余次数必须 >= 0 (没透支)// 2. 这是一个“更优”的状态:比之前到达该点时剩的 k 更多if (next_k >= 0 && next_k > visited[nr][nc]) {visited[nr][nc] = next_k; // 更新最优状态q.push({nr, nc, next_k});}}}}steps++;}return -1;}
};
深度复杂度分析
V (Vertices):这里的“节点”实际上是
(row, col, k)的组合。状态总数:
m * n * k(因为k最多也就m*n级别,或者题目给定较小)。时间复杂度 O(m * n * min(k, m*n)):
在最坏情况下,我们可能需要访问每个
(r, c)位置,并且带着从0到k各种不同的剩余消除次数。实际上,由于贪心剪枝和
visited的最优值更新逻辑,实际运行会快很多。
空间复杂度 O(m * n * min(k, m*n)):
主要是队列和
visited数组的开销。这里visited是二维的O(m*n),但队列中可能存很多状态。
总结:BFS 的“三维”世界
今天这道题,让我们对 BFS 的理解从“平面”走向了“立体”。
普通 BFS:在地图上找最短路,状态是
(x, y)。带状态 BFS:在地图上找最短路,但受到资源限制,状态是
(x, y, resource)。
“限制步数”,只要看到此种约束,请立刻想起:就是这种**“将资源约束纳入状态”**的思想,是解决复杂最短路困难的核心。无论是“消除障碍”、“携带钥匙”还我要给 BFS 升维了!
我们已经完成了网格图论的所有核心挑战!从下一篇开始,我们将进入图论的另一个主要领域——拓扑排序,去消除那些关于“依赖”与“顺序”的谜题。
下期见!