图论专题(十五):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),可能有两种情况:

  1. 路径 A :绕了一大圈到了 (r, c),没消除任何障碍(剩余消除次数 k)。

  2. 路径 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 <vector>
#include <queue>

using namespace std;

class Solution {
public:
    int shortestPath(vector<vector<int>>& 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<vector<int>> q;
        q.push({0, 0, k});

        // 状态记录:visited[r][c] 记录到达 (r, c) 时剩余的最大消除次数
        // 初始化为 -1
        vector<vector<int>> visited(m, vector<int>(n, -1));
        visited[0][0] = k;

        int steps = 0;
        int dr[] = {0, 0, 1, -1};
        int dc[] = {1, -1, 0, 0};

        // 2. 启动 BFS
        while (!q.empty()) {
            int size = q.size();
            
            // 遍历当前层
            for (int i = 0; i < size; ++i) {
                vector<int> 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 - 1
                        int 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) 位置,并且带着从 0k 各种不同的剩余消除次数。

    • 实际上,由于贪心剪枝和 visited 的最优值更新逻辑,实际运行会快很多。

  • 空间复杂度 O(m * n * min(k, m*n))

    • 主要是队列和 visited 数组的开销。这里 visited 是二维的 O(m*n),但队列中可能存很多状态。

总结:BFS 的"三维"世界

今天这道题,让我们对 BFS 的理解从"平面"走向了"立体"。

  • 普通 BFS :在地图上找最短路,状态是 (x, y)

  • 带状态 BFS :在地图上找最短路,但受到资源限制,状态是 (x, y, resource)

这种**"将资源约束纳入状态"**的思想,是解决复杂最短路问题的核心。无论是"消除障碍"、"携带钥匙"还是"限制步数",只要看到这种约束,请立刻想起:我要给 BFS 升维了!

我们已经完成了网格图论 的所有核心挑战!从下一篇开始,我们将进入图论的另一个重要领域------拓扑排序,去解决那些关于"依赖"与"顺序"的谜题。

下期见!

相关推荐
NAGNIP11 小时前
万字长文!回归模型最全讲解!
算法·面试
知乎的哥廷根数学学派11 小时前
面向可信机械故障诊断的自适应置信度惩罚深度校准算法(Pytorch)
人工智能·pytorch·python·深度学习·算法·机器学习·矩阵
666HZ66613 小时前
数据结构2.0 线性表
c语言·数据结构·算法
余瑜鱼鱼鱼13 小时前
Java数据结构:从入门到精通(十二)
数据结构
实心儿儿13 小时前
Linux —— 基础开发工具5
linux·运维·算法
charlie11451419114 小时前
嵌入式的现代C++教程——constexpr与设计技巧
开发语言·c++·笔记·单片机·学习·算法·嵌入式
清木铎15 小时前
leetcode_day4_筑基期_《绝境求生》
算法
清木铎15 小时前
leetcode_day10_筑基期_《绝境求生》
算法
j_jiajia16 小时前
(一)人工智能算法之监督学习——KNN
人工智能·学习·算法
源代码•宸16 小时前
Golang语法进阶(协程池、反射)
开发语言·经验分享·后端·算法·golang·反射·协程池