中等
一个机器人位于一个 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)
- 入口 (Entry):
-
- 从终点
(m-1, n-1)开始调用dfs,逆流而上寻找起点。 - 初始化
memo数组,0 表示未计算(因为路径数至少为 1,所以 0 是安全的初始值)。
- 从终点
- 递归 (Recursion):
-
- 越界检查 :
i < 0或j < 0,说明撞墙了,这条路不通,返回 0。 - Base Case :
i == 0 && j == 0,终于回溯到了起点,说明找到了一条合法路径,返回 1。 - 查表 :
memo[i][j] != 0,说明这个格子的路径数算过了,直接返回。
- 越界检查 :
- 计算 (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)。
- 启动 dfs(2, 1):
-
- 需要
dfs(1, 1)(上) +dfs(2, 0)(左)。
- 需要
- 分支 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。
- 需要
- 分支 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。
- 需要
- 汇总 dfs(2, 1):
-
dfs(1, 1)(2) +dfs(2, 0)(1) = 3。
- 最终结果: 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 迭代)
- 定义 (Def):
-
- 创建
f[m + 1][n + 1]。多出来的一行一列默认是 0,充当"墙壁"。
- 创建
- 点火 (Seed):
-
f[0][1] = 1。这是为了给起点(0,0)提供初始值的。- 或者设置
f[1][0] = 1也可以,效果一样。
- 填表 (Loop):
-
i从 0 到m-1,j从 0 到n-1。f[i+1][j+1](当前格) =f[i][j+1](上) +f[i+1][j](左)。- 由于有了哨兵,循环内完全不需要
if (i > 0)这种边界检查。
- 结果 (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 (列)
- 初始化:
-
f是 4x3 的矩阵。全 0。- 设置
f[0][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。 (一直往右走)
- 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。 (上+左)
- 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。
- 返回 :
f[3][2] = 3。