#62 二维DP 中等

不同路径

在 LeetCode 上查看 ↗

🎧 语音讲解

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

速度

📋 题目描述

一个机器人位于一个 m x n 网格的左上角(起始点标记为「Start」)。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(标记为「Finish」)。

问总共有多少条不同的路径?

示例 1

输入:m = 3, n = 7
输出:28

示例 2

输入:m = 3, n = 2
输出:3
从左上角开始,总共有 3 条路径可以到达右下角:右→下→下、下→下→右、下→右→下。

示例 3

输入:m = 3, n = 3
输出:6

💭 模拟答题者思考

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

2. 重复在哪里?到达 (i,j) 的路径,最后一步要么从 (i-1,j) 来,要么从 (i,j-1) 来;两种来源的路径条数互不相交,可以相加——典型的最优子结构。

3. 子问题定义:设 dp[i][j] = 到 (i,j) 的路径数。递推 dp[i][j] = dp[i-1][j] + dp[i][j-1];边界第一行/列全为 1(只能一直走一个方向)。

4. 手推 m=3, n=2:DP 表为 [[1,1],[1,2],[1,3]],右下角 dp[2][1]=3,与示例 2 一致。

5. 组合数学视角:总共走 m+n-2 步,其中 m-1 步向下,答案为 C(m+n-2, m-1);但 DP 思路更通用(#63 有障碍物时组合公式不好直接套)。

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

变量类型语义(三句法)
dp[i][j]int[][]定义:从左上角 (0,0) 走到格子 (i,j) 的不同路径条数
维护:每个格子只能从上方或左方来,故 dp[i][j] = dp[i-1][j] + dp[i][j-1]
更新:按行优先双重循环,i 从 0 到 m-1j 从 0 到 n-1 递增填表
dp[0][*] / dp[*][0]int定义:第一行、第一列格子的路径数边界
维护:起点到同行/同列任意格只能一直向右或一直向下,每条路径唯一
更新:全部初始化为 1;若 i==0j==0 时直接设 dp[i][j]=1,无需递推
prev / curint[]定义:空间优化时的一维滚动数组,cur[j] 表示当前行第 j 列的路径数
维护:每算完一行,prev = cur 作为下一行的「上方」来源
更新cur[j] = prev[j] + cur[j-1](上方 + 左方),首列 cur[0]=1
m, nint定义:网格的行数与列数
维护:只读输入,决定 DP 表规模和最终答案位置 dp[m-1][n-1]
更新:范围 1 ≤ m,n ≤ 100,答案不超过 2×10⁹

⌨️ 落码步骤

1. 创建 dp[m][n],第一行、第一列全部填 1

2. 双重循环 i 从 1 到 m-1j 从 1 到 n-1

3. dp[i][j] = dp[i-1][j] + dp[i][j-1]

4. 返回 dp[m-1][n-1]

5. (可选)空间优化:用长度 n 的一维数组滚动,每行更新 cur[j] = prev[j] + cur[j-1]

💻 代码实现

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        # dp[j]:当前行第 j 列的路径数(滚动数组)
        dp = [1] * n
        for i in range(1, m):
            for j in range(1, n):
                dp[j] += dp[j - 1]  # 上方(prev[j]) + 左方(dp[j-1])
        return dp[n - 1]
class Solution {
public:
    int uniquePaths(int m, int n) {
        vector<int> dp(n, 1);  // 第一行全为 1
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                dp[j] += dp[j - 1];  // 上方 + 左方
            }
        }
        return dp[n - 1];
    }
};
// 时间 O(mn),空间 O(n)

📈 复杂度分析

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

⚠️ 常见坑

边界未初始化:第一行、第一列的路径数都是 1,不是 0;若漏初始化,dp[1][1] 会从 0 递推出错。

递推方向错误:必须从左到右、从上到下填表,保证 dp[i-1][j]dp[i][j-1] 已算好;倒序遍历会得到错误答案。

滚动数组时覆盖顺序:内层 j 必须从 1 到 n-1 递增,dp[j] += dp[j-1] 中的 dp[j-1] 是本行左邻(已更新),dp[j] 更新前保存的是上一行同列值。

🔍 必测边界 Case

Case 1:单行
m = 1, n = 5 → 1(只能一直向右,唯一路径)
Case 2:单列
m = 5, n = 1 → 1(只能一直向下,唯一路径)
Case 3:最小网格
m = 1, n = 1 → 1(起点即终点)
Case 4:正方形
m = 3, n = 3 → 6(对称情形,验证递推正确性)
Case 5:扁长网格
m = 3, n = 7 → 28(示例 1,路径数 = C(8,2) = 28)