网格中的最短路径(可消除障碍) 图解题解
这道题到底在问什么
- 输入
- grid=[[0,0,0],[1,1,0],[0,0,0],[0,1,1],[0,0,0]], k=1
- 输出
- 6
最优解:一步一步想明白
- 3记住「状态带上剩余消除次数、best 存每格见过的最大 rem、只有更大才入队」,下面逐帧套它。
- 4从起点 (0,0) 出发,手里有 1 次消除机会,记 best[0][0]=1、步数 0,放进队列。格子上标的 k1 就是「到这里时还剩几次消除」。
- 5轮到队首 (0,0)(紫):此刻剩余消除次数 1、已走 0 步。看它上下左右四个邻居,决定哪些值得走。
- 6往下的 (1,0) 是障碍,消除它(剩余 1 减 1 = 0),记 best=0、步数 1 入队(标 k0)。
- 7往右的 (0,1) 是空地,剩余不变还是 1,而且比这格旧记录更优,记 best=1、步数 1 入队(标 k1)。
- 8轮到队首 (1,0)(紫):此刻剩余消除次数 0、已走 1 步。看它上下左右四个邻居,决定哪些值得走。
- 9往下的 (2,0) 是空地,剩余不变还是 0,而且比这格旧记录更优,记 best=0、步数 2 入队(标 k0)。
- 10往上的 (0,0):走过去剩余 还是 0,但之前到这格时剩余有 1、比现在多。剩得多的那次后劲更足,这次更差,不必再入队。
- 11往右的 (1,1) 是障碍(墙),可手里剩余消除次数已经是 0、消不动了,这个方向只能放弃。
- 12轮到队首 (0,1)(紫):此刻剩余消除次数 1、已走 1 步。看它上下左右四个邻居,决定哪些值得走。
- 13往下的 (1,1) 是障碍,消除它(剩余 1 减 1 = 0),记 best=0、步数 2 入队(标 k0)。
- 14往右的 (0,2) 是空地,剩余不变还是 1,而且比这格旧记录更优,记 best=1、步数 2 入队(标 k1)。
- 15往左的 (0,0):走过去剩余 还是 1,而之前已经用同样的剩余 1 到过这里了,这次并不更优;判重规则是只有剩余严格更大才入队,相等也要跳过。
- 16轮到队首 (2,0)(紫):此刻剩余消除次数 0、已走 2 步。看它上下左右四个邻居,决定哪些值得走。
- 17往下的 (3,0) 是空地,剩余不变还是 0,而且比这格旧记录更优,记 best=0、步数 3 入队(标 k0)。
- 18往上的 (1,0) 是障碍(墙),可手里剩余消除次数已经是 0、消不动了,这个方向只能放弃。
- 19往右的 (2,1) 是空地,剩余不变还是 0,而且比这格旧记录更优,记 best=0、步数 3 入队(标 k0)。
- 20轮到队首 (1,1)(紫):此刻剩余消除次数 0、已走 2 步。看它上下左右四个邻居,决定哪些值得走。
- 21往下的 (2,1):走过去剩余 还是 0,而之前已经用同样的剩余 0 到过这里了,这次并不更优;判重规则是只有剩余严格更大才入队,相等也要跳过。
- 22往上的 (0,1):走过去剩余 还是 0,但之前到这格时剩余有 1、比现在多。剩得多的那次后劲更足,这次更差,不必再入队。
- 23往右的 (1,2) 是空地,剩余不变还是 0,而且比这格旧记录更优,记 best=0、步数 3 入队(标 k0)。
- 24往左的 (1,0) 是障碍(墙),可手里剩余消除次数已经是 0、消不动了,这个方向只能放弃。
- 25轮到队首 (0,2)(紫):此刻剩余消除次数 1、已走 2 步。看它上下左右四个邻居,决定哪些值得走。
- 26往下的 (1,2) 是空地,剩余不变还是 1,而且比这格旧记录更优,记 best=1、步数 3 入队(标 k1)。
- 27往左的 (0,1):走过去剩余 还是 1,而之前已经用同样的剩余 1 到过这里了,这次并不更优;判重规则是只有剩余严格更大才入队,相等也要跳过。
- 28轮到队首 (3,0)(紫):此刻剩余消除次数 0、已走 3 步。看它上下左右四个邻居,决定哪些值得走。
- 29往下的 (4,0) 是空地,剩余不变还是 0,而且比这格旧记录更优,记 best=0、步数 4 入队(标 k0)。
- 30往上的 (2,0):走过去剩余 还是 0,而之前已经用同样的剩余 0 到过这里了,这次并不更优;判重规则是只有剩余严格更大才入队,相等也要跳过。
- 31往右的 (3,1) 是障碍(墙),可手里剩余消除次数已经是 0、消不动了,这个方向只能放弃。
- 32轮到队首 (2,1)(紫):此刻剩余消除次数 0、已走 3 步。看它上下左右四个邻居,决定哪些值得走。
- 33往下的 (3,1) 是障碍(墙),可手里剩余消除次数已经是 0、消不动了,这个方向只能放弃。
- 34往上的 (1,1) 是障碍(墙),可手里剩余消除次数已经是 0、消不动了,这个方向只能放弃。
- 35往右的 (2,2) 是空地,剩余不变还是 0,而且比这格旧记录更优,记 best=0、步数 4 入队(标 k0)。
- 36往左的 (2,0):走过去剩余 还是 0,而之前已经用同样的剩余 0 到过这里了,这次并不更优;判重规则是只有剩余严格更大才入队,相等也要跳过。
- 37轮到队首 (1,2)(紫):此刻剩余消除次数 0、已走 3 步。看它上下左右四个邻居,决定哪些值得走。
- 38往下的 (2,2):走过去剩余 还是 0,而之前已经用同样的剩余 0 到过这里了,这次并不更优;判重规则是只有剩余严格更大才入队,相等也要跳过。
- 39往上的 (0,2):走过去剩余 还是 0,但之前到这格时剩余有 1、比现在多。剩得多的那次后劲更足,这次更差,不必再入队。
- 40往左的 (1,1) 是障碍(墙),可手里剩余消除次数已经是 0、消不动了,这个方向只能放弃。
- 41轮到队首 (1,2)(紫):此刻剩余消除次数 1、已走 3 步。看它上下左右四个邻居,决定哪些值得走。
- 42往下的 (2,2) 是空地,剩余不变还是 1,而且比这格旧记录更优,记 best=1、步数 4 入队(标 k1)。
- 43往上的 (0,2):走过去剩余 还是 1,而之前已经用同样的剩余 1 到过这里了,这次并不更优;判重规则是只有剩余严格更大才入队,相等也要跳过。
- 44往左的 (1,1):走过去剩余 消一个后剩 0,而之前已经用同样的剩余 0 到过这里了,这次并不更优;判重规则是只有剩余严格更大才入队,相等也要跳过。
- 45轮到队首 (4,0)(紫):此刻剩余消除次数 0、已走 4 步。看它上下左右四个邻居,决定哪些值得走。
- 46往上的 (3,0):走过去剩余 还是 0,而之前已经用同样的剩余 0 到过这里了,这次并不更优;判重规则是只有剩余严格更大才入队,相等也要跳过。
- 47往右的 (4,1) 是空地,剩余不变还是 0,而且比这格旧记录更优,记 best=0、步数 5 入队(标 k0)。
- 48轮到队首 (2,2)(紫):此刻剩余消除次数 0、已走 4 步。看它上下左右四个邻居,决定哪些值得走。
- 49往下的 (3,2) 是障碍(墙),可手里剩余消除次数已经是 0、消不动了,这个方向只能放弃。
- 50往上的 (1,2):走过去剩余 还是 0,但之前到这格时剩余有 1、比现在多。剩得多的那次后劲更足,这次更差,不必再入队。
- 51往左的 (2,1):走过去剩余 还是 0,而之前已经用同样的剩余 0 到过这里了,这次并不更优;判重规则是只有剩余严格更大才入队,相等也要跳过。
- 52轮到队首 (2,2)(紫):此刻剩余消除次数 1、已走 4 步。看它上下左右四个邻居,决定哪些值得走。
- 53往下的 (3,2) 是障碍,消除它(剩余 1 减 1 = 0),记 best=0、步数 5 入队(标 k0)。
- 54往上的 (1,2):走过去剩余 还是 1,而之前已经用同样的剩余 1 到过这里了,这次并不更优;判重规则是只有剩余严格更大才入队,相等也要跳过。
- 55往左的 (2,1) 是空地,剩余不变还是 1,而且比这格旧记录更优,记 best=1、步数 5 入队(标 k1)。
- 56轮到队首 (4,1)(紫):此刻剩余消除次数 0、已走 5 步。看它上下左右四个邻居,决定哪些值得走。
- 57往上的 (3,1) 是障碍(墙),可手里剩余消除次数已经是 0、消不动了,这个方向只能放弃。
- 58往右就是终点 (4,2)(绿)!BFS 第一次走到终点,当前步数 5 再加这一步 = 6,就是答案。
- 59从终点沿父指针回溯,把这条最短路点亮(绿)。它一共 6 步,中途消除了路上的障碍。回头看:正因为状态带了「剩余消除次数」、用 best 记每格见过的最大 rem,BFS 才没把「剩得多」和「剩得少」当成同一种「已访问」,从而找到真正的最短路。
⚠️ 容易写错的地方
✗ 错:判重只看坐标 (r,c)
✓ 对:判重看 (r,c) 上见过的最大剩余 rem
同一格、剩余次数不同,后续能力不同;只按坐标判重会把「剩得多」的更优状态挡在门外,导致错过最短路
✗ 错:best 记最小 rem 或随便记
✓ 对:best 记到该格见过的最大 rem,只有更大才入队
剩余越多越有余地消障碍,留最大的那次才不漏解
✗ 错:到终点还按普通格更新 best 再绕
✓ 对:第一次 BFS 走到终点立即返回当前步数
BFS 层数单调,首次到终点就是最短,不必继续
完整代码(Python / C++ / Java)
Python
from typing import List
from collections import deque
class Solution:
def shortestPath(self, grid: List[List[int]], k: int) -> int:
m, n = len(grid), len(grid[0])
if m == 1 and n == 1:
return 0
best = [[-1] * n for _ in range(m)]
best[0][0] = k
q = deque([(0, 0, k, 0)])
while q:
r, c, rem, dist = q.popleft()
for dr, dc in ((1,0),(-1,0),(0,1),(0,-1)):
nr, nc = r + dr, c + dc
if 0 <= nr < m and 0 <= nc < n:
nrem = rem - grid[nr][nc]
if nrem < 0:
continue
if nr == m - 1 and nc == n - 1:
return dist + 1
if nrem > best[nr][nc]:
best[nr][nc] = nrem
q.append((nr, nc, nrem, dist + 1))
return -1C++
#include <queue>
#include <vector>
using namespace std;
class Solution {
public:
int shortestPath(vector<vector<int>>& grid, int k) {
int m = grid.size(), n = grid[0].size();
if (m == 1 && n == 1) return 0;
vector<vector<int>> best(m, vector<int>(n, -1));
queue<array<int,4>> q;
q.push({0,0,k,0}); best[0][0] = k;
int dirs[5] = {1,0,-1,0,1};
while (!q.empty()) {
auto [r, c, rem, dist] = q.front(); q.pop();
for (int d = 0; d < 4; ++d) {
int nr = r + dirs[d], nc = c + dirs[d+1];
if (nr >= 0 && nr < m && nc >= 0 && nc < n) {
int nrem = rem - grid[nr][nc];
if (nrem < 0) continue;
if (nr == m - 1 && nc == n - 1) return dist + 1;
if (nrem > best[nr][nc]) { best[nr][nc] = nrem; q.push({nr, nc, nrem, dist + 1}); }
}
}
}
return -1;
}
};Java
import java.util.*;
class Solution {
public int shortestPath(int[][] grid, int k) {
int m = grid.length, n = grid[0].length;
if (m == 1 && n == 1) return 0;
int[][] best = new int[m][n];
for (int[] row : best) Arrays.fill(row, -1);
Queue<int[]> q = new ArrayDeque<>();
q.offer(new int[]{0,0,k,0}); best[0][0] = k;
int[] dirs = {1,0,-1,0,1};
while (!q.isEmpty()) {
int[] cur = q.poll();
for (int d = 0; d < 4; d++) {
int nr = cur[0] + dirs[d], nc = cur[1] + dirs[d + 1];
if (nr >= 0 && nr < m && nc >= 0 && nc < n) {
int rem = cur[2] - grid[nr][nc];
if (rem < 0) continue;
if (nr == m - 1 && nc == n - 1) return cur[3] + 1;
if (rem > best[nr][nc]) { best[nr][nc] = rem; q.offer(new int[]{nr, nc, rem, cur[3] + 1}); }
}
}
}
return -1;
}
}复杂度
时间
O(m·n·k)
状态总数最多是「格子数 m·n」乘「剩余次数 0..k 共 k+1 种」;每个状态只在 rem 更大时入队一次、各看 4 个邻居,所以是 O(m·n·k)
空间
O(m·n·k)
best 表是 O(m·n);但状态多了一维剩余次数 rem,同一格可能随不同的 rem 多次入队,队列/状态上界是 O(m·n·k),取整体最坏 O(m·n·k)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 网格中的最短路径(可消除障碍) 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
能不能用 visited 集合存 (r,c,rem) 三元组代替 best 表?+
可以,效果一样:把每个 (r,c,rem) 三元组放进 visited,没见过才入队。best[r][c] 存最大 rem 是它的优化版——因为对同一格,rem 越大越优,只需记最大的那个,比存所有三元组更省。两种写法都对。
为什么这题用 BFS 而不是 Dijkstra?+
因为每步代价都是 1(走一格就是一步),边权相等的最短路用 BFS 按层扩展即可,层数就是步数,不需要优先队列。只有当不同步代价不同时才需要 Dijkstra。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 网格中的最短路径(可消除障碍) 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。