【LeetCode: 3286. 穿越网格图的安全路径 + BFS】

|-----------|
| 🚀 算法题 🚀 |

🌲 算法刷题专栏 | 面试必备算法 | 面试高频算法 🍀

🌲 越难的东西,越要努力坚持,因为它具有很高的价值,算法就是这样✨

🌲 作者简介:硕风和炜,CSDN-Java领域优质创作者🏆,保研|国家奖学金|高中学习JAVA|大学完善JAVA开发技术栈|面试刷题|面经八股文|经验分享|好用的网站工具分享💎💎💎

🌲 恭喜你发现一枚宝藏博主,赶快收入囊中吧🌻

🌲 人生如棋,我愿为卒,行动虽慢,可谁曾见我后退一步?🎯🎯

|-----------|
| 🚀 算法题 🚀 |

🍔 目录

    • [🚩 题目链接](#🚩 题目链接)
    • [⛲ 题目描述](#⛲ 题目描述)
    • [🌟 求解思路&实现代码&运行结果](#🌟 求解思路&实现代码&运行结果)
      • [🥦 题目要求](#🥦 题目要求)
      • [🥦 核心逻辑拆解](#🥦 核心逻辑拆解)
      • [🥦 实现代码](#🥦 实现代码)
      • [🥦 关键原理说明](#🥦 关键原理说明)
        • [🎯 为什么不能只用坐标去重?](#🎯 为什么不能只用坐标去重?)
        • [🎯 vis 数组剪枝核心原理](#🎯 vis 数组剪枝核心原理)
        • [🎯 BFS 相比 DFS 的优势](#🎯 BFS 相比 DFS 的优势)
        • 🎯血量判断时机
      • [🥦 运行效率](#🥦 运行效率)
    • [💬 共勉](#💬 共勉)

🚩 题目链接

⛲ 题目描述

给你一个 m x n 的二进制矩形 grid 和一个整数 health 表示你的健康值。

你开始于矩形的左上角 (0, 0) ,你的目标是矩形的右下角 (m - 1, n - 1) 。

你可以在矩形中往上下左右相邻格子移动,但前提是你的健康值始终是 正数 。

对于格子 (i, j) ,如果 gridij = 1 ,那么这个格子视为 不安全 的,会使你的健康值减少 1 。

如果你可以到达最终的格子,请你返回 true ,否则返回 false 。

注意 ,当你在最终格子的时候,你的健康值也必须为 正数 。

示例 1:

输入:grid = \[0,1,0,0,0,0,1,0,1,0,0,0,0,1,0], health = 1

输出:true

解释:

沿着下图中灰色格子走,可以安全到达最终的格子。
示例 2:


输入:grid = \[0,1,1,0,0,0,1,0,1,0,0,0,0,1,1,1,0,1,0,0,1,0,1,0], health = 3

输出:false

解释:

健康值最少为 4 才能安全到达最后的格子。
示例 3:

输入:grid = \[1,1,1,1,0,1,1,1,1], health = 5

输出:true

解释:

沿着下图中灰色格子走,可以安全到达最终的格子。

任何不经过格子 (1, 1) 的路径都是不安全的,因为你的健康值到达最终格子时,都会小于等于 0 。

提示:

  • m == grid.length
  • n == gridi.length
  • 1 <= m, n <= 50
  • 2 <= m * n
  • 1 <= health <= m + n
  • gridij 要么是 0 ,要么是 1 。

🌟 求解思路&实现代码&运行结果


🥦 题目要求

🎯题目描述

给定二维网格 grid,网格中每个格子值仅为 0 或 1:

  1. 格子为 0:安全区域,经过不扣除血量;格子为 1:危险区域,经过扣除 1 点血量。
  2. 起点固定为网格左上角 (0,0),终点为网格右下角 (m-1,n-1)。
  3. 移动规则:仅能向上下左右四个相邻格子移动,不可越出网格边界。
  4. 血量约束:初始血量为 health,踏入格子瞬间扣除对应血量;任意时刻剩余血量必须严格大于 0,血量 ≤ 0 则该路径失效。
  5. 函数返回布尔值:是否存在一条从起点抵达终点的合法安全路径。
🎯边界限制
  • 网格尺寸范围:1 <= m,n <= 50
  • 血量范围:1 <= health <= 100

🥦 核心逻辑拆解

本题使用 BFS 广度优先搜索求解,整体逻辑分为 5 个核心步骤:

  1. 初始化前置处理
    先计算起点格子扣除血量后的剩余血量,若起点扣血后血量直接 ≤0,无需搜索直接返回 false;
  2. 状态记录数组设计
    定义二维数组 vis,visxy 存储曾经到达坐标 (x,y) 的最大剩余血量,用于剪枝去重;
  3. 队列存储搜索状态
    队列中三元组 x, y, remainHp 分别代表当前坐标、当前剩余血量,起点入队开启搜索;
  4. 循环出队遍历四向邻居
    每次取出队首位置,若当前坐标为终点直接返回 true;否则遍历上下左右四个方向;
  5. 邻居合法性校验与入队规则
  • 越界直接跳过;
  • 踏入新格子扣血,新血量 ≤0 直接舍弃该分支;
  • 仅当新血量大于历史记录 visnxny 时更新记录并入队,避免重复无效搜索;
  1. 队列遍历完毕未抵达终点
    所有可行路径全部遍历完成仍未到达终点,返回 false。

🥦 实现代码

java 复制代码
class Solution {
    // 四向偏移
    private final int[][] dirs = { { -1, 0 }, { 1, 0 }, { 0, -1 }, { 0, 1 } };

    public boolean findSafeWalk(List<List<Integer>> grid, int health) {
        int m = grid.size();
        int n = grid.get(0).size();
        // vis[x][y]:记录到达(x,y)时曾经拥有过的最大剩余血量
        int[][] vis = new int[m][n];
        // 队列元素:x坐标, y坐标, 当前剩余血量
        Queue<int[]> q = new LinkedList<>();

        // 起点扣血
        int startCost = grid.get(0).get(0);
        int remain = health - startCost;
        // 起点直接血量<=0,直接失败
        if (remain <= 0)
            return false;
        vis[0][0] = remain;
        q.add(new int[] { 0, 0, remain });

        while (!q.isEmpty()) {
            int[] cur = q.poll();
            int x = cur[0], y = cur[1], hp = cur[2];

            // 到达终点,找到可行路径
            if (x == m - 1 && y == n - 1) {
                return true;
            }

            // 遍历四个方向
            for (int[] d : dirs) {
                int nx = x + d[0];
                int ny = y + d[1];
                // 越界判断
                if (nx < 0 || nx >= m || ny < 0 || ny >= n)
                    continue;
                // 走到新格子扣血
                int newHp = hp - grid.get(nx).get(ny);
                // 血量<=0直接舍弃这条路径
                if (newHp <= 0)
                    continue;
                // 去重:之前到(nx,ny)的血量比现在更高,不用走
                if (newHp > vis[nx][ny]) {
                    vis[nx][ny] = newHp;
                    q.add(new int[] { nx, ny, newHp });
                }
            }
        }
        // 所有路径遍历完都没到终点
        return false;
    }
}

🥦 关键原理说明

🎯 为什么不能只用坐标去重?
  1. 常规网格迷宫题仅记录坐标访问状态,本题不适用。
  2. 同一坐标可以多次经过:第一次经过剩余血量 2,第二次经过剩余血量 4。血量更高的第二次路径容错性更强,后续更容易走到终点,不能直接标记访问后永久封锁该坐标。
🎯 vis 数组剪枝核心原理
  1. visxy 保存到达该点的最大剩余血量:
  • 若新路径到达 (x,y) 的血量 ≤ visxy:这条路径血量更少,后续只会更难存活,无需入队;
  • 若新路径血量 > visxy:当前路径更优,更新血量并加入队列继续搜索;
  1. 该机制彻底杜绝来回绕圈的无限循环,大幅减少搜索状态。
🎯 BFS 相比 DFS 的优势
  1. DFS 递归实现在网格尺寸极大时会触发栈溢出;
  2. BFS 使用队列迭代实现,无递归深度限制,稳定性更强。同时本题仅需判断是否存在路径,BFS 能快速找到任意一条合法路径即可提前返回,平均效率更优。
🎯血量判断时机
  1. 必须踏入格子后立刻扣血并校验血量,包含起点与终点;
  2. 终点格子同样会造成血量扣除,扣完血量必须仍大于 0 才算有效路径,是本题高频易错点。

🥦 运行效率

🎯 时间复杂度

网格最大 50×50,血量上限 100,总状态上限 50 * 50 * 100 = 250000;每个状态最多入队一次,每个状态仅遍历 4 个方向,总运算量极小。

时间复杂度:O(m×n×H),m、n 为网格行列,H 为初始血量。

🎯空间复杂度
  • 队列存储搜索状态:最坏存储全部合法状态 O(m * n * H);
  • 剪枝数组 vis:固定二维数组 O(m * n);
  • 综合空间复杂度:O(m * n * H),题目数据范围下内存占用极低,无超时风险。

💬 共勉

|----------------------------------|
| 最后,我想和大家分享一句一直激励我的座右铭,希望可以与大家共勉! |