bigywater.tech
算法与数据结构 · LeetCode 63

LeetCode 63. 不同路径 II

和上一题一样,机器人从网格左上角走到右下角,只能向右或向下;不同的是网格里有障碍(1 表示障碍,0 表示空地),路径不能经过障碍。动态规划的思路不变:走到 (i, j) 的路径数等于上面那格加左边那格,但障碍格子的路径数是 0。边界情形需要小心:起点本身是障碍、终点本身是障碍、第一行或第一列被障碍截断。本文用真实数据展示"第一行第一列一律初始化为 1"的写法会错多少,验证动态规划与枚举全部路径的答案一致,给出五种语言的实现,并把带障碍的填表做成分步演示。

20,000
组随机对拍,与暴力解的结果全部一致
18 步
分步演示,每一帧都来自解法的真实执行,另附 5 种语言实现

题意与示例

完整题目见官方页面(链接在文末)。基准解是递归枚举所有路径(不做记忆化,只适合小网格),遇到障碍或越界返回 0,到终点返回 1,与逐格累加是两种独立的写法。一共 12 组用例,覆盖:官方示例、起点是障碍、终点是障碍、第一行有障碍、整行被障碍堵住、单行、单列、没有障碍的方阵:

真实实测 · 本机执行输出
输入 [[[0,0,0],[0,1,0],[0,0,0]]]
输出 2
输入 [[[0,1],[0,0]]]
输出 1
输入 [[[1]]]
输出 0
输入 [[[0]]]
输出 1
输入 [[[0,0],[0,1]]]
输出 0
输入 [[[1,0]]]
输出 0

陷阱:第一行和第一列不能一律填 1

没有障碍时,第一行、第一列每个格子都只有一条路,所以上一题可以把它们全部初始化为 1。有障碍时就不对了:第一行里某个格子是障碍,那么它右边所有的格子都走不到(只能从左边来);第一列同理。下面是"第一行第一列一律初始化为 1"的写法与 solve 的真实结果:

真实实测 · 本机执行输出
# 第一行、第一列一律初始化为 1 的写法
[[0, 1, 0]]: 该写法 -> 1    solve -> 0
[[0], [1], [0]]: 该写法 -> 1    solve -> 0
[[0, 0], [1, 0]]: 该写法 -> 2    solve -> 1
[[0, 0, 0], [0, 1, 0], [0, 0, 0]]: 该写法 -> 2    solve -> 2
[[1, 0], [0, 0]]: 该写法 -> 2    solve -> 0
随机 20000 个网格:该写法与正确答案不同的有 11536 个(57.7%)

[[0, 1, 0]](一行,中间是障碍)应该是 0,那种写法得到 1;[[1, 0], [0, 0]](起点是障碍)应该是 0,得到 2。最后一行的统计是:在随机生成的 20000 个网格里(每个格子 25% 是障碍),这种写法与正确答案不同的有 11536 个,占 57.7%。而官方示例([[0, 0, 0], [0, 1, 0], [0, 0, 0]])两种写法都是 2,因为障碍不在第一行第一列,所以只用官方示例测不出这个错误。

一维动态规划:五种语言的实现

dp[j] 是走到当前行第 j 列的路径数。初始化:dp[0] 在起点不是障碍时是 1,否则是 0,其余是 0。按行、按列遍历:遇到障碍,dp[j] = 0;否则如果不是第一列,dp[j] += dp[j - 1](dp[j] 原来的值是从上面来,dp[j - 1] 是从左边来)。第一列不是障碍时 dp[0] 保持上一行的值,也就是从上面来的路径数。最后返回 dp[n - 1]。Python、Java、Go、JavaScript、C++ 都是真实编译运行的。

Python · 解法代码def solve(grid): m, n = len(grid), len(grid[0]) dp = [0] * n # dp[j]:走到当前行第 j 列的路径数 dp[0] = 1 if grid[0][0] == 0 else 0 # 起点本身是障碍就一条路都没有 for i in range(m): for j in range(n): if grid[i][j] == 1: dp[j] = 0 # 障碍格子:没有路能走到这里 elif j > 0: dp[j] += dp[j - 1] # 到 (i, j) = 从上面来(旧的 dp[j])+ 从左边来(dp[j-1]) return dp[n - 1]
真实实测 · Python · 前几组用例的输出
$ python3 solution.py   # 统一入口:逐行读取 cases.jsonl,调用上面的 solve,打印 JSON
[[[0,0,0],[0,1,0],[0,0,0]]]  →  2
[[[0,1],[0,0]]]  →  1
[[[1]]]  →  0
[[[0]]]  →  1
[[[0,0],[0,1]]]  →  0
[[[1,0]]]  →  0
Java · 解法代码class Solution { public int uniquePathsWithObstacles(int[][] grid) { int m = grid.length, n = grid[0].length; int[] dp = new int[n]; // dp[j]:走到当前行第 j 列的路径数 dp[0] = grid[0][0] == 0 ? 1 : 0; // 起点本身是障碍就一条路都没有 for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (grid[i][j] == 1) dp[j] = 0; // 障碍格子:没有路能走到这里 else if (j > 0) dp[j] += dp[j - 1]; // 到 (i, j) = 从上面来(旧的 dp[j])+ 从左边来(dp[j-1]) } } return dp[n - 1]; } }
真实实测 · Java · 前几组用例的输出
$ javac -d build Main.java Solution.java && java -cp build Main < cases.jsonl
[[[0,0,0],[0,1,0],[0,0,0]]]  →  2
[[[0,1],[0,0]]]  →  1
[[[1]]]  →  0
[[[0]]]  →  1
[[[0,0],[0,1]]]  →  0
[[[1,0]]]  →  0
Go · 解法代码func uniquePathsWithObstacles(grid [][]int) int { m, n := len(grid), len(grid[0]) dp := make([]int, n) // dp[j]:走到当前行第 j 列的路径数 if grid[0][0] == 0 { // 起点本身是障碍就一条路都没有 dp[0] = 1 } for i := 0; i < m; i++ { for j := 0; j < n; j++ { if grid[i][j] == 1 { dp[j] = 0 // 障碍格子:没有路能走到这里 } else if j > 0 { dp[j] += dp[j-1] // 到 (i, j) = 从上面来(旧的 dp[j])+ 从左边来(dp[j-1]) } } } return dp[n-1] }
真实实测 · Go · 前几组用例的输出
$ go build -o bin . && ./bin < cases.jsonl
[[[0,0,0],[0,1,0],[0,0,0]]]  →  2
[[[0,1],[0,0]]]  →  1
[[[1]]]  →  0
[[[0]]]  →  1
[[[0,0],[0,1]]]  →  0
[[[1,0]]]  →  0
JavaScript · 解法代码function uniquePathsWithObstacles(grid) { const m = grid.length, n = grid[0].length; const dp = new Array(n).fill(0); // dp[j]:走到当前行第 j 列的路径数 dp[0] = grid[0][0] === 0 ? 1 : 0; // 起点本身是障碍就一条路都没有 for (let i = 0; i < m; i++) { for (let j = 0; j < n; j++) { if (grid[i][j] === 1) dp[j] = 0; // 障碍格子:没有路能走到这里 else if (j > 0) dp[j] += dp[j - 1]; // 到 (i, j) = 从上面来(旧的 dp[j])+ 从左边来(dp[j-1]) } } return dp[n - 1]; }
真实实测 · JavaScript · 前几组用例的输出
$ node run.js < cases.jsonl
[[[0,0,0],[0,1,0],[0,0,0]]]  →  2
[[[0,1],[0,0]]]  →  1
[[[1]]]  →  0
[[[0]]]  →  1
[[[0,0],[0,1]]]  →  0
[[[1,0]]]  →  0
C++ · 解法代码#include <vector> using namespace std; class Solution { public: int uniquePathsWithObstacles(vector<vector<int>>& grid) { int m = grid.size(), n = grid[0].size(); vector<int> dp(n, 0); // dp[j]:走到当前行第 j 列的路径数 dp[0] = grid[0][0] == 0 ? 1 : 0; // 起点本身是障碍就一条路都没有 for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (grid[i][j] == 1) dp[j] = 0; // 障碍格子:没有路能走到这里 else if (j > 0) dp[j] += dp[j - 1]; // 到 (i, j) = 从上面来(旧的 dp[j])+ 从左边来(dp[j-1]) } } return dp[n - 1]; } };
真实实测 · C++ · 前几组用例的输出
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin < cases.jsonl
[[[0,0,0],[0,1,0],[0,0,0]]]  →  2
[[[0,1],[0,0]]]  →  1
[[[1]]]  →  0
[[[0]]]  →  1
[[[0,0],[0,1]]]  →  0
[[[1,0]]]  →  0
真实实测 · 本机执行输出
共 12 组用例(含边界),以暴力解的结果为基准逐组比对:
  Python      通过 12/12
  Java        通过 12/12
  Go          通过 12/12
  JavaScript  通过 12/12
  C++         通过 12/12

耗时实测:动态规划对递归枚举

网格边长 m = n 分别为 6、8、10、12,网格里有规律地分布着障碍,每个规模取三次中最快的一次:

真实实测 · 本机执行输出
m = n(每隔几格有一个障碍) | 暴力解 | 本文解法
         6 |     0.034 ms | 0.002 ms
         8 |     0.244 ms | 0.003 ms
        10 |     2.074 ms | 0.004 ms
        12 |    13.862 ms | 0.006 ms

递归枚举(基准)的耗时随网格增大快速增长:n = 12 时 13.9ms,比 n = 10 时的 2.07ms 多了约 6.7 倍;动态规划几乎不变,n = 12 时只有 0.006ms。这个网格上障碍较多,路径数没有那么大;没有障碍时,枚举会更慢,具体见上一题的递归调用次数。

随机对拍

Python 版本另外和递归枚举做了随机对拍:行数、列数各取 1 到 6,每个格子 25% 的概率是障碍,起点和终点也可能是障碍:

真实实测 · 本机执行输出
随机对拍 20000 组(行数、列数各取 1 到 6,每个格子有 25% 的概率是障碍(起点、终点也可能是障碍)),solve 与 brute 的结果全部一致。

另外四种语言只在上面 12 组固定用例上验证过,没有做大规模随机对拍。

分步演示:障碍格子的路径数是 0

下面的演示用 4×4 的网格,有两个障碍(用 X 表示)。逐格计算:每个空地格子等于上面加左边,障碍格子是 0。障碍会让它右边、下边的路径数变少,最后右下角的值就是答案。每一帧都是把上面的解法在这组输入上真实执行记录下来的,运行器还检查了演示的最终结果与 solve 的返回值一致。

不同路径 II:4×4 网格,1 是障碍
正在计算的格子已算好的格子障碍

    参考与说明

    • 官方题目页:LeetCode 63. 不同路径 II。本文只用自己的话概括了题意,完整约束以官方页面为准。
    • 数据来自本机真实运行,五种语言的版本写在页脚;耗时对比只用 Python 测量。
    • 没有覆盖:把障碍网格原地当作 dp 表使用的写法;路径数超过 32 位整数时的处理。
    京ICP备2026067502号-1京公网安备11010502063487号