#63 二维DP 中等

不同路径 II

在 LeetCode 上查看 ↗

🎧 语音讲解

开车或通勤时可听,跟着思路走一遍

速度

📋 题目描述

给定一个 m x n 的整数数组 grid。一个机器人初始位于 左上角(即 grid[0][0])。机器人尝试移动到 右下角(即 grid[m - 1][n - 1])。机器人每次只能向下或者向右移动一步。

网格中的障碍物和空位置分别用 10 来表示。机器人的移动路径中不能包含 任何 有障碍物的方格。

返回机器人能够到达右下角的不同路径数量。

测试用例保证答案小于等于 2 × 109

示例 1

输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
3×3 网格正中间有障碍物。从左上角到右下角共 2 条路径:右→右→下→下、下→下→右→右。

示例 2

输入:obstacleGrid = [[0,1],[0,0]]
输出:1

💭 模拟答题者思考

1. 最直接:DFS/BFS 枚举所有「只向右或向下」的路径,跳过障碍格,每走到右下角计数 +1——正确但指数级,m,n 可达 100 会超时。

2. 重复在哪里?到达 (i,j) 的路径,最后一步要么从 (i-1,j) 来,要么从 (i,j-1) 来——与 #62 相同的最优子结构;但障碍格不能站脚,到达它的路径数为 0。

3. 子问题定义:设 dp[i][j] = 到 (i,j) 的路径数。若 grid[i][j]==1dp[i][j]=0;否则 dp[i][j]=dp[i-1][j]+dp[i][j-1]。边界:第一行/列只能从起点单向延伸,遇障碍则后续同向格子全为 0。

4. 手推示例 1:中间 (1,1) 为障碍 dp[1][1]=0dp[2][2]=dp[1][2]+dp[2][1]=1+1=2,与输出一致。

5. 特判起点:若 grid[0][0]==1,机器人无法出发,直接返回 0——这是 #62 没有的额外边界。

🧠 变量语义(先读这三句再编码)

变量类型语义(三句法)
dp[i][j]int[][]定义:从左上角 (0,0) 走到格子 (i,j) 且路径不经过障碍的不同路径条数
维护:若 grid[i][j]==1dp[i][j]=0;否则 dp[i][j]=dp[i-1][j]+dp[i][j-1]
更新:按行优先双重循环,i 从 0 到 m-1j 从 0 到 n-1 递增填表
grid[i][j]int定义:格子状态,0 可通行、1 为障碍物
维护:只读输入,决定当前格子能否作为路径终点
更新:遇到 1 时直接将 dp[i][j] 置 0,不再累加上方/左方
dp[0][*] / dp[*][0]int定义:第一行、第一列格子的路径数边界
维护:从起点沿同行/同列只能一直向右或向下;途中遇障碍则该格及之后同向格子均为 0
更新dp[0][0]=1(若起点非障碍);i==0dp[0][j]=dp[0][j-1](遇障为 0);j==0dp[i][0]=dp[i-1][0]
prev / curint[]定义:空间优化时的一维滚动数组,cur[j] 表示当前行第 j 列的路径数
维护:每算完一行,prev = cur 作为下一行的「上方」来源
更新:障碍格 cur[j]=0;否则 cur[j] += prev[j](左方已在上一轮循环累加)

⌨️ 落码步骤

1. 若 grid[0][0]==1,返回 0

2. 创建 dp[n] 滚动数组,dp[0]=1,按行遍历 i 从 0 到 m-1

3. 内层 j 从 0 到 n-1:若 grid[i][j]==1,设 dp[j]=0;否则若 j>0dp[j]+=dp[j-1](上方值已在 dp[j] 中)

4. 返回 dp[n-1]

5. 二维写法:双重循环填 dp[i][j],边界行/列单独处理「遇障截断」逻辑

💻 代码实现

class Solution:
    def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
        if obstacleGrid[0][0] == 1:
            return 0
        m, n = len(obstacleGrid), len(obstacleGrid[0])
        dp = [0] * n
        dp[0] = 1
        for i in range(m):
            for j in range(n):
                if obstacleGrid[i][j] == 1:
                    dp[j] = 0
                elif j > 0:
                    dp[j] += dp[j - 1]  # 上方(旧dp[j]) + 左方(dp[j-1])
        return dp[n - 1]
class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        if (obstacleGrid[0][0] == 1) return 0;
        int m = obstacleGrid.size(), n = obstacleGrid[0].size();
        vector<int> dp(n, 0);
        dp[0] = 1;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (obstacleGrid[i][j] == 1) {
                    dp[j] = 0;
                } else if (j > 0) {
                    dp[j] += dp[j - 1];  // 上方 + 左方
                }
            }
        }
        return dp[n - 1];
    }
};
// 时间 O(mn),空间 O(n)

📈 复杂度分析

时间复杂度 O(m × n)
空间复杂度 O(n)(滚动数组)

⚠️ 常见坑

起点有障碍未特判:grid[0][0]==1 时应返回 0,否则 dp[0][0] 会被错误地当作 1。

障碍格仍做递推:遇到 grid[i][j]==1 必须将 dp[i][j] 置 0,不能累加上方/左方,否则会把「经过障碍」的路径计入。

第一行/列边界照抄 #62 全填 1:有障碍时同向后续格子应为 0(路径被截断),需用 dp[0][j]=dp[0][j-1] 而非固定 1。

🔍 必测边界 Case

Case 1:起点即障碍
[[1,0],[0,0]] → 0(机器人无法出发)
Case 2:终点有障碍
[[0,0],[0,1]] → 0(无法到达终点格)
Case 3:无障碍
[[0,0],[0,0]] → 2(退化为 #62 的 2×2 网格)
Case 4:单行遇障截断
[[0,1,0,0]] → 0(障碍阻断向右延伸,无法到达最右格)
Case 5:仅一条通路
[[0,1],[0,0]] → 1(示例 2,必须绕开第一行障碍)