LeetCode 中等难度题目「63. 不同路径 II」,这道题是「不同路径」的进阶版,核心多了「障碍物」的限制,看似复杂,其实只要掌握动态规划的核心逻辑,就能轻松拿下。本文会从题目分析、思路推导、代码解析到注意事项,一步步帮大家搞懂。
一、题目回顾:读懂题意,明确边界
先再仔细看一遍题目,避免理解偏差:
-
给定一个 m x n 的网格 grid,机器人从左上角(grid[0][0])出发,要走到右下角(grid[m-1][n-1])。
-
机器人每次只能 向下 或 向右 移动一步,不能走其他方向。
-
网格中 1 表示障碍物,0 表示空位置,机器人路径中 不能包含任何障碍物。
-
返回能到达右下角的不同路径数量,测试用例保证答案不超过 2*10⁹(不用考虑溢出问题)。
举个简单例子:如果网格是 [[0,0],[1,0]],机器人从 (0,0) 出发,只能向右到 (0,1),再向下到 (1,1),只有 1 条路径;如果起点或终点本身是障碍物(grid[0][0]=1 或 grid[m-1][n-1]=1),直接返回 0。
二、解题思路:为什么选动态规划?
这道题和「不同路径」(无障碍物)的核心区别的是「障碍物会阻断路径」,但解题核心依然是 动态规划(DP),原因很简单:
机器人走到某一格 (i,j) 的路径数,只和它 左边一格 (i,j-1) 和 上边一格 (i-1,j) 的路径数有关------因为机器人只能从左边或上边过来。这就是「无后效性」,也是动态规划的核心前提。
1. 定义DP数组
我们定义 dp[i][j] 表示:到达网格中第 i 行、第 j 列(下标从 0 开始)的不同路径数量。
2. 确定递推公式
分两种核心情况:
-
如果当前格子 (i,j) 是障碍物(grid[i][j] = 1):那么机器人无法到达这里,所以 dp[i][j] = 0。
-
如果当前格子 (i,j) 是空位置(grid[i][j] = 0):那么到达这里的路径数 = 到达左边格子的路径数 + 到达上边格子的路径数,即:
dp[i][j] = dp[i][j-1] + dp[i-1][j]
这里要注意边界处理:当 i=0 时(第一行),机器人只能从左边过来,不能从上边过来(没有上边);当 j=0 时(第一列),机器人只能从上边过来,不能从左边过来(没有左边)。如果第一行/第一列中有障碍物,那么障碍物后面的格子都无法到达(路径数为 0)。
3. 初始化DP数组
题目中给定的代码,初始化时用了 Array.from({ length: m }, () => new Array(n).fill(Infinity)),这里的 Infinity 只是一个"占位符",后续会通过循环覆盖掉,核心初始化其实是「起点 (0,0)」:
如果起点 (0,0) 不是障碍物(grid[0][0] = 0),那么 dp[0][0] = 1(只有1种方式到达起点);如果起点是障碍物,后续所有格子都无法到达,最终返回 0。
三、完整代码解析:逐行看懂每一步
先贴出题目中给出的完整代码,再逐行拆解,帮大家搞懂每一步的作用:
typescript
function uniquePathsWithObstacles(obstacleGrid: number[][]): number {
const m = obstacleGrid.length;
const n = obstacleGrid[0].length;
const dp = Array.from({ length: m }, () => new Array(n).fill(Infinity));
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (obstacleGrid[i][j] === 1) {
dp[i][j] = 0;
continue;
}
if (i === 0 && j === 0) {
dp[i][j] = 1;
continue;
}
dp[i][j] = (j - 1 >= 0 && dp[i][j - 1]) + (i - 1 >= 0 && dp[i - 1][j]);
}
}
return dp[m - 1][n - 1];
};
逐行解析(重点看注释)
-
const m = obstacleGrid.length;:获取网格的行数 m(比如 3 行网格,m=3)。 -
const n = obstacleGrid[0].length;:获取网格的列数 n(比如 4 列网格,n=4)。 -
const dp = Array.from({ length: m }, () => new Array(n).fill(Infinity));:创建一个 m x n 的 DP 数组,初始值填充为 Infinity(占位用,后续会覆盖)。这里不用 0 初始化,是为了避免和"障碍物的路径数 0"混淆,当然用 0 初始化也可以,只要后续逻辑处理到位。 -
for (let i = 0; i < m; i++) { for (let j = 0; j < n; j++) { ... } }:双重循环,遍历网格中的每一个格子(从左上角到右下角,逐行逐列)。 -
if (obstacleGrid[i][j] === 1) { dp[i][j] = 0; continue; }:如果当前格子是障碍物,直接设 dp[i][j] = 0(无法到达),跳过后续逻辑。 -
if (i === 0 && j === 0) { dp[i][j] = 1; continue; }:如果是起点(0,0),且不是障碍物,设 dp[0][0] = 1(只有1种方式到达起点)。 -
dp[i][j] = (j - 1 >= 0 && dp[i][j - 1]) + (i - 1 >= 0 && dp[i - 1][j]);:核心递推公式,拆解一下:-
j - 1 >= 0 && dp[i][j - 1]:判断左边格子是否存在(j-1 >= 0),如果存在,取左边格子的路径数;如果不存在(比如 j=0,第一列),则这部分为 0(逻辑与运算,false 对应 0)。 -
i - 1 >= 0 && dp[i - 1][j]:判断上边格子是否存在(i-1 >= 0),如果存在,取上边格子的路径数;如果不存在(比如 i=0,第一行),则这部分为 0。 -
两者相加,就是当前格子的路径数,完美处理了边界问题(第一行、第一列)。
-
-
return dp[m - 1][n - 1];:返回右下角格子的路径数,也就是最终答案。
四、测试用例验证:帮你避开坑
光看懂代码还不够,我们用几个测试用例验证一下,看看代码是否能正确处理各种情况:
测试用例1:起点是障碍物
输入:obstacleGrid = [[1,0],[0,0]]
解析:起点 (0,0) 是障碍物,机器人无法出发,返回 0。
代码运行结果:0(正确)。
测试用例2:终点是障碍物
输入:obstacleGrid = [[0,0],[0,1]]
解析:终点 (1,1) 是障碍物,无论怎么走都到不了,返回 0。
代码运行结果:0(正确)。
测试用例3:第一行有障碍物
输入:obstacleGrid = [[0,0,1,0],[0,0,0,0],[0,0,0,0]]
解析:第一行第3列(下标2)是障碍物,第一行中障碍物右边的格子(0,3)无法到达(路径数0),最终右下角路径数为 3。
代码运行结果:3(正确)。
测试用例4:常规无障碍物(等同于 LeetCode 62题)
输入:obstacleGrid = [[0,0,0],[0,0,0],[0,0,0]]
解析:3x3网格,无障碍物,不同路径数为 6(和62题答案一致)。
代码运行结果:6(正确)。
五、优化方向:空间复杂度优化(可选)
上面的代码空间复杂度是 O(m*n)(用了一个 m x n 的 DP 数组),其实可以优化到 O(n)(或 O(m)),因为我们每次计算 dp[i][j],只需要用到「上一行的 dp 值」和「当前行左边的 dp 值」,不需要保留整个二维数组。
优化思路:用一个一维数组 dp,长度为 n,每次遍历一行时,更新 dp[j] 的值:
-
初始化 dp[0] = 1(如果起点不是障碍物)。
-
遍历第一行:如果当前格子是障碍物,dp[j] = 0,且后续所有 dp[j] 都为 0(因为第一行只能从左边过来)。
-
遍历后续行:对于每个 j,dp[j] = (当前格子是否是障碍物?0 : (dp[j] + dp[j-1])),其中 dp[j] 是上一行的结果,dp[j-1] 是当前行左边的结果。
优化后的代码(TypeScript):
typescript
function uniquePathsWithObstacles(obstacleGrid: number[][]): number {
const m = obstacleGrid.length;
const n = obstacleGrid[0].length;
const dp = new Array(n).fill(0);
// 初始化起点
dp[0] = obstacleGrid[0][0] === 0 ? 1 : 0;
// 遍历第一行
for (let j = 1; j < n; j++) {
dp[j] = obstacleGrid[0][j] === 0 ? dp[j-1] : 0;
}
// 遍历后续行
for (let i = 1; i < m; i++) {
// 第一列特殊处理(只能从上边过来)
dp[0] = obstacleGrid[i][0] === 0 ? dp[0] : 0;
for (let j = 1; j < n; j++) {
dp[j] = obstacleGrid[i][j] === 0 ? dp[j] + dp[j-1] : 0;
}
}
return dp[n-1];
};
优化后空间复杂度从 O(mn) 降到 O(n),时间复杂度依然是 O(mn)(还是要遍历所有格子),在网格较大时,这种优化会更实用。
六、常见坑点:新手必看
做这道题时,很多新手会踩以下几个坑,一定要注意:
-
忘记处理「起点或终点是障碍物」的情况:如果 grid[0][0] = 1 或 grid[m-1][n-1] = 1,直接返回 0,否则会计算错误。
-
边界处理不当:第一行只能从左边过来,第一列只能从上边过来,一旦其中有障碍物,后续格子的路径数都是 0,不要漏了这个逻辑。
-
DP数组初始化错误:如果用 0 初始化 DP 数组,要注意起点的初始化(dp[0][0] = 1),否则所有格子的路径数都会是 0。
-
混淆「障碍物的路径数」和「边界的路径数」:障碍物的路径数是 0,而边界上非障碍物的路径数是 1(比如第一行第一个非障碍物格子,路径数是 1)。
七、总结:解题核心提炼
这道题的核心是「动态规划 + 边界处理 + 障碍物判断」,记住三句话就能搞定:
-
DP定义:dp[i][j] 表示到达 (i,j) 的路径数。
-
递推公式:障碍物 → 0;非障碍物 → 左边路径数 + 上边路径数(注意边界)。
-
初始化:起点非障碍物则为 1,否则为 0;边界上的格子,障碍物后全为 0。
无论是原始的 O(mn) 空间解法,还是优化后的 O(n) 空间解法,核心逻辑都是一样的,大家可以根据自己的需求选择。建议先掌握原始解法,再理解优化思路,这样更容易吃透动态规划的本质。