LeetCode 980困难回溯/DFS
不同路径 III 图解题解
这道题到底在问什么
给定网格 grid,1=起点、2=终点、0=空地、−1=障碍。返回从起点到终点、且恰好走遍每一个非障碍格各一次的不同路径数量。
- 输入
- [[1,0,0,0],[0,0,0,0],[0,0,2,-1]]
- 输出
- 2(两条路覆盖全部 11 个空格)
最优解:一步一步想明白
- 3记住「踩格标记 → 四向递归 → 退格还原」,到 E 时剩余待走格为 0 才计一条。下面在官方样例上一步步走。
- 4先扫一遍网格:非障碍格共 11 个(这就是必须全部踩到的格子数),起点 S 在 (0,0),障碍 X 在 (2,3) 不能走。
- 5从起点 S 出发,踩进格子 (0,0):把它标记为已占用(紫色 = 正踩、黄色 = 路径已占用的格),剩余待走 left 减一。
- 6继续往邻居走,踩进格子 (1,0):把它标记为已占用(紫色 = 正踩、黄色 = 路径已占用的格),剩余待走 left 减一。
- 7继续往邻居走,踩进格子 (2,0):把它标记为已占用(紫色 = 正踩、黄色 = 路径已占用的格),剩余待走 left 减一。
- 8继续往邻居走,踩进格子 (2,1):把它标记为已占用(紫色 = 正踩、黄色 = 路径已占用的格),剩余待走 left 减一。
- 9这条线很快摸到了终点 E,可是回头一看:还有 6 个空格没踩过!「恰好走遍每个格」没满足,这不是合法路径,要原路退回去换方向。
- 10回溯的关键动作:把刚才标记过的格子一个个还原成可走(撤销标记、left 加回去),这样换个方向时这些格子还能被别的路径走到。
- 11踩进 (0,0)(紫色),目前已占用 1 个格、还剩 10 个待走。继续朝没走过的邻居探。
- 12踩进 (1,0)(紫色),目前已占用 2 个格、还剩 9 个待走。继续朝没走过的邻居探。
- 13踩进 (2,0)(紫色),目前已占用 3 个格、还剩 8 个待走。继续朝没走过的邻居探。
- 14踩进 (2,1)(紫色),目前已占用 4 个格、还剩 7 个待走。继续朝没走过的邻居探。
- 15踩进 (1,1)(紫色),目前已占用 5 个格、还剩 6 个待走。继续朝没走过的邻居探。
- 16踩进 (0,1)(紫色),目前已占用 6 个格、还剩 5 个待走。继续朝没走过的邻居探。
- 17踩进 (0,2)(紫色),目前已占用 7 个格、还剩 4 个待走。继续朝没走过的邻居探。
- 18踩进 (0,3)(紫色),目前已占用 8 个格、还剩 3 个待走。继续朝没走过的邻居探。
- 19踩进 (1,3)(紫色),目前已占用 9 个格、还剩 2 个待走。继续朝没走过的邻居探。
- 20踩进 (1,2)(紫色),目前已占用 10 个格、还剩 1 个待走。继续朝没走过的邻居探。
- 21这一次踩满了全部 11 个空格才到 E,剩余待走恰好为 0,第 1 条合法路径成立,答案计数 +1。绿色就是这条路径。
- 22第 1 条记下后并不停手:回溯把网格还原,再从起点换一个出发方向,看还有没有别的走法能踩满全部格子。
- 23这条线先沿第一行走:踩进 (0,0),已占用 1 格、还剩 10 格待走。
- 24这条线先沿第一行走:踩进 (0,1),已占用 2 格、还剩 9 格待走。
- 25这条线先沿第一行走:踩进 (0,2),已占用 3 格、还剩 8 格待走。
- 26这条线先沿第一行走:踩进 (0,3),已占用 4 格、还剩 7 格待走。
- 27这条线先沿第一行走:踩进 (1,3),已占用 5 格、还剩 6 格待走。
- 28这条线先沿第一行走:踩进 (1,2),已占用 6 格、还剩 5 格待走。
- 29这条线先沿第一行走:踩进 (1,1),已占用 7 格、还剩 4 格待走。
- 30这条线先沿第一行走:踩进 (1,0),已占用 8 格、还剩 3 格待走。
- 31这条线先沿第一行走:踩进 (2,0),已占用 9 格、还剩 2 格待走。
- 32这条线先沿第一行走:踩进 (2,1),已占用 10 格、还剩 1 格待走。
- 33换的这条线同样踩满全部 11 个空格、剩余为 0,第 2 条合法路径成立,计数再 +1。它和第 1 条经过的顺序不同,是另一条路。
- 34所有方向都试完、再没有新的合法走法。整个过程只有两条路径踩满了全部空格,所以答案是 2。
⚠️ 容易写错的地方
✗ 错:到终点就直接计数
✓ 对:到终点还要校验「剩余待走格 = 0」
题目要求走遍每个空格各一次;提前到终点而有格子没走过,不算合法路径
✗ 错:走过的格子不还原
✓ 对:递归返回后必须把当前格还原
不还原 = 这格被永久占用,别的分支再也走不到它,会漏掉合法路径
✗ 错:把障碍格也算进 todo
✓ 对:统计时跳过 −1
todo 是「必须走遍的非障碍格数」,障碍不参与,算错会导致终点判定全错
完整代码(Python / C++ / Java)
Python
from typing import List
class Solution:
def uniquePathsIII(self, grid: List[List[int]]) -> int:
m, n = len(grid), len(grid[0])
todo = 0
sr = sc = 0
for r in range(m):
for c in range(n):
if grid[r][c] != -1:
todo += 1
if grid[r][c] == 1:
sr, sc = r, c
def dfs(r, c, left):
if r < 0 or r >= m or c < 0 or c >= n or grid[r][c] == -1:
return 0
if grid[r][c] == 2:
return 1 if left == 1 else 0
tmp = grid[r][c]
grid[r][c] = -1
ans = dfs(r+1,c,left-1) + dfs(r-1,c,left-1) + dfs(r,c+1,left-1) + dfs(r,c-1,left-1)
grid[r][c] = tmp
return ans
return dfs(sr, sc, todo)C++
#include <vector>
using namespace std;
class Solution {
public:
int uniquePathsIII(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size(), todo = 0, sr = 0, sc = 0;
for (int r = 0; r < m; ++r) for (int c = 0; c < n; ++c) {
if (grid[r][c] != -1) todo++;
if (grid[r][c] == 1) { sr = r; sc = c; }
}
function<int(int,int,int)> dfs = [&](int r, int c, int left) {
if (r < 0 || r >= m || c < 0 || c >= n || grid[r][c] == -1) return 0;
if (grid[r][c] == 2) return left == 1 ? 1 : 0;
int old = grid[r][c];
grid[r][c] = -1;
int ans = dfs(r+1,c,left-1) + dfs(r-1,c,left-1) + dfs(r,c+1,left-1) + dfs(r,c-1,left-1);
grid[r][c] = old;
return ans;
};
return dfs(sr, sc, todo);
}
};Java
import java.util.*;
class Solution {
int[][] grid; int m, n;
public int uniquePathsIII(int[][] grid) {
this.grid = grid; m = grid.length; n = grid[0].length;
int todo = 0, sr = 0, sc = 0;
for (int r = 0; r < m; r++) for (int c = 0; c < n; c++) {
if (grid[r][c] != -1) todo++;
if (grid[r][c] == 1) { sr = r; sc = c; }
}
return dfs(sr, sc, todo);
}
private int dfs(int r, int c, int left) {
if (r < 0 || r >= m || c < 0 || c >= n || grid[r][c] == -1) return 0;
if (grid[r][c] == 2) return left == 1 ? 1 : 0;
int old = grid[r][c]; grid[r][c] = -1;
int ans = dfs(r+1,c,left-1) + dfs(r-1,c,left-1) + dfs(r,c+1,left-1) + dfs(r,c-1,left-1);
grid[r][c] = old;
return ans;
}
}复杂度
时间
O(3^k)
k = 非障碍格数;除来路外每格最多向 3 个方向延伸
空间
O(k)
递归栈深度最多为路径长度 k
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 不同路径 III 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题为什么敢用指数级的回溯,不会超时吗?+
看数据范围:题目保证非障碍格数量 ≤ 20,搜索空间被这个上限死死压住;加上「越界 / 踩到障碍 / 已走过」立即剪枝,实际探索的分支远小于理论上界,足以通过。范围小是「敢暴力回溯」的信号。
能不能用状态压缩 + 记忆化优化?+
可以。把「当前位置 + 已访问格子的集合(用一个二进制位掩码表示哪些格走过)」作为状态做记忆化 DP,复杂度降到 O(k · 2^k)。因为非障碍格 ≤ 20,位掩码刚好放得下,这是该题进阶的标准优化方向。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 不同路径 III 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。