62. 不同路径

62. 不同路径

中等

一个机器人位于一个 m x n网格的左上角 (起始点在下图中标记为 "Start" )。

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

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

示例 1:

复制代码
输入:m = 3, n = 7
输出:28

示例 2:

复制代码
输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下

示例 3:

复制代码
输入:m = 7, n = 3
输出:28

示例 4:

复制代码
输入:m = 3, n = 3
输出:6

提示:

  • 1 <= m, n <= 100
  • 题目数据保证答案小于等于 2 * 109

📝 核心笔记:不同路径 (Unique Paths - DFS)

1. 核心思想 (一句话总结)

"溯源游戏:我想知道走到终点 **(i, j)**有几条路,就问问它的'上家'------它头顶的格子 **(i-1, j)**和左边的格子 **(i, j-1)**各有几条路。"

  • 状态定义dfs(i, j) 表示从起点 (0, 0) 走到 (i, j) 的路径总数。
  • 转移方程dfs(i, j) = dfs(上) + dfs(左)
  • 加法原理:路径总数等于所有可能的来源之和。
2. 算法流程 (DFS + Memo)
  1. 入口 (Entry)
    • 从终点 (m-1, n-1) 开始调用 dfs,逆流而上寻找起点。
    • 初始化 memo 数组,0 表示未计算(因为路径数至少为 1,所以 0 是安全的初始值)。
  1. 递归 (Recursion)
    • 越界检查i < 0j < 0,说明撞墙了,这条路不通,返回 0。
    • Base Casei == 0 && j == 0,终于回溯到了起点,说明找到了一条合法路径,返回 1。
    • 查表memo[i][j] != 0,说明这个格子的路径数算过了,直接返回。
  1. 计算 (Calculate)
    • res = dfs(i-1, j) + dfs(i, j-1)
    • 存入 memo[i][j] 并返回。
🔍 代码回忆清单
复制代码
// 题目:LC 62. Unique Paths
class Solution {
    public int uniquePaths(int m, int n) {
        // memo[i][j]: 到达坐标 (i, j) 的路径数
        // 0 表示未计算,因为只要能到,路径数至少是 1
        int[][] memo = new int[m][n];
        
        // 从终点开始往回推
        return dfs(m - 1, n - 1, memo);
    }

    private int dfs(int i, int j, int[][] memo) {
        // 1. 越界检查 (撞墙)
        // 比如在第一行往上看 (i-1 < 0),是没有路的
        if (i < 0 || j < 0) {
            return 0;
        }
        
        // 2. Base Case: 回到了起点
        // 找到了一条路,贡献 1 个计数
        if (i == 0 && j == 0) {
            return 1;
        }
        
        // 3. 记忆化:查表
        if (memo[i][j] != 0) { 
            return memo[i][j];
        }
        
        // 4. 状态转移:来自上面 + 来自左边
        return memo[i][j] = dfs(i - 1, j, memo) + dfs(i, j - 1, memo);
    }
}
⚡ 快速复习 CheckList (易错点)
  • \] **为什么 Base Case 返回 1?**

    • 因为 (0,0) 本身就是一种"到达"的状态(或者说不需要移动就在起点了)。
    • 数学上,加法的单位元是 0(越界),乘法的单位元是 1(组合数)。这里是累加路径,当递归触底时,代表"这是一条有效路径",所以计数 +1。
  • \] **时间复杂度?**

    • 虽然是递归,但有 Memo 保证每个格子只计算一次。
    • 这和双重循环填 DP 表的计算量是完全一样的。
  • \] **能不能优化空间?**

    • 递归写法因为栈深度的原因,空间复杂度是 (数组) + (栈)。
    • 如果面试官要求优化空间,需要改写成 迭代版 (DP) + 滚动数组,可以将空间降为 。
🖼️ 中文数字演练

m = 3 (行), n = 2 (列)

终点是 (2, 1)

  1. 启动 dfs(2, 1):
    • 需要 dfs(1, 1) (上) + dfs(2, 0) (左)。
  1. 分支 A: dfs(1, 1):
    • 需要 dfs(0, 1) + dfs(1, 0)
    • dfs(0, 1) (第一行往右): 最终追溯到起点,返回 1
    • dfs(1, 0) (第一列往下): 最终追溯到起点,返回 1
    • 结果: 1 + 1 = 2。记录 memo[1][1] = 2
  1. 分支 B: dfs(2, 0): (最下面一行)
    • 需要 dfs(1, 0) (上) + dfs(2, -1) (左, 越界)。
    • dfs(1, 0): 之前算过吗?如果在递归树中没共享,会重算追溯到起点,返回 1
    • dfs(2, -1): 返回 0
    • 结果: 1 + 0 = 1。记录 memo[2][0] = 1
  1. 汇总 dfs(2, 1):
    • dfs(1, 1) (2) + dfs(2, 0) (1) = 3
  1. 最终结果: 3。

📝 核心笔记:不同路径 (Unique Paths - DP Iterative) 递推

1. 核心思想 (一句话总结)

"网格填数:每一个格子的路径数,都等于它'正上方格子'和'正左方格子'的路径数之和(汇流原理)。"

  • 状态定义f[i+1][j+1] 对应实际网格 (i, j) 的路径数。
  • 转移方程f[curr] = f[up] + f[left]
  • 哨兵技巧f[0][1] = 1 是唯一的"火种"。当计算起点 (0,0) 对应的 f[1][1] 时,它会等于 f[0][1] (1) + f[1][0] (0) = 1,从而成功初始化起点。
2. 算法流程 (DP 迭代)
  1. 定义 (Def)
    • 创建 f[m + 1][n + 1]。多出来的一行一列默认是 0,充当"墙壁"。
  1. 点火 (Seed)
    • f[0][1] = 1。这是为了给起点 (0,0) 提供初始值的。
    • 或者设置 f[1][0] = 1 也可以,效果一样。
  1. 填表 (Loop)
    • i 从 0 到 m-1j 从 0 到 n-1
    • f[i+1][j+1](当前格) = f[i][j+1](上) + f[i+1][j](左)。
    • 由于有了哨兵,循环内完全不需要 if (i > 0) 这种边界检查。
  1. 结果 (Result)
    • 返回 f[m][n]
🔍 代码回忆清单
复制代码
// 题目:LC 62. Unique Paths
class Solution {
    public int uniquePaths(int m, int n) {
        // 1. DP 表:多开一行一列,避免处理边界条件
        // 默认初始化为 0
        int[][] f = new int[m + 1][n + 1];
        
        // 2. Base Case (哨兵点火)
        // 这是一个虚拟的"起点的上方",值为 1
        // 当计算 f[1][1] (即起点) 时,它会变成 1 + 0 = 1
        f[0][1] = 1; 
        
        // 3. 遍历实际的网格坐标 (0 到 m-1, 0 到 n-1)
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                // 4. 状态转移
                // i+1, j+1 是 DP 表中的坐标
                // i, j+1 是"上",i+1, j 是"左"
                f[i + 1][j + 1] = f[i][j + 1] + f[i + 1][j];
            }
        }
        
        // 5. 返回右下角
        return f[m][n];
    }
}
⚡ 快速复习 CheckList (易错点)
  • \] **为什么** **f****的大小是** **m+1, n+1****?**

    • 为了在处理 f[1][...] (实际第0行) 时,能直接访问 f[0][...] 而不越界。f[0] 行全是 0,相当于网格外的墙壁,除了我们特意设置的那个 1。
  • \] **f[0][1] = 1****放在循环里行吗?**

    • 不行。它必须在循环开始前设置好,作为推导的源头。
    • 如果放在循环里,每次都重置,逻辑就乱了。
  • \] **空间复杂度优化?**

    • 当前是 。
    • 可以优化成一维数组 int[] f = new int[n + 1] (滚动数组)。
    • f[j] = f[j] + f[j-1] (新值 = 旧值(上) + 新值(左))。
🖼️ 数字演练

m = 3 (行), n = 2 (列)

  1. 初始化:
    • f 是 4x3 的矩阵。全 0。
    • 设置 f[0][1] = 1
  1. i=0 (实际第1行):
    • j=0: f[1][1] = f[0][1](1) + f[1][0](0) = 1。 (起点)
    • j=1: f[1][2] = f[0][2](0) + f[1][1](1) = 1。 (一直往右走)
  1. i=1 (实际第2行):
    • j=0: f[2][1] = f[1][1](1) + f[2][0](0) = 1。 (一直往下走)
    • j=1: f[2][2] = f[1][2](1) + f[2][1](1) = 2。 (上+左)
  1. i=2 (实际第3行):
    • j=0: f[3][1] = f[2][1](1) + f[3][0](0) = 1
    • j=1: f[3][2] = f[2][2](2) + f[3][1](1) = 3
  1. 返回 : f[3][2] = 3
相关推荐
小资同学1 小时前
考研机试 -Kruskal算法
算法
y芋泥啵啵gfe2 小时前
AI考研深造VS直接工作:选对赛道,认证为竞争力加码
人工智能·职场和发展
big_rabbit05022 小时前
[算法][力扣283]Move Zeros
算法·leetcode·职场和发展
小资同学2 小时前
考研机试动态规划 线性DP
算法·动态规划
栗子~~2 小时前
hardhat 单元测试时如何观察gas消耗情况
开发语言·单元测试·区块链·智能合约
listhi5202 小时前
两台三相逆变器并联功率分配控制MATLAB实现
算法
The hopes of the whole village2 小时前
Matlab FFT分析
开发语言·matlab
Evand J2 小时前
【IMM】非线性目标跟踪算法与MATLAB实现:基于粒子滤波的交互式多模型,结合CV和CT双模型对三维空间中的机动目标进行高精度跟踪
算法·matlab·目标跟踪·pf·粒子滤波·imm·多模型
重生之后端学习2 小时前
64. 最小路径和
数据结构·算法·leetcode·排序算法·深度优先·图论