文章目录
题目理解
我们可以把网格看成一个棋盘:
- 起点:左上角
- 终点:右下角
- 允许动作:
向右或向下
以 m = 3, n = 7 为例,答案是 28。
一个直观网格示意:
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | |
|---|---|---|---|---|---|---|---|
| 0 | S | ||||||
| 1 | |||||||
| 2 | E |
解法总览
不同路径
暴力递归
思路直观
指数级复杂度
记忆化搜索
递归 + 缓存
避免重复子问题
动态规划
二维DP
状态转移清晰
一维DP
空间优化
数学组合
本质是排列组合
复杂度最低
关键状态与转移
定义 f(i, j) 表示从起点走到格子 (i,j) 的路径数。
因为最后一步只能来自:
- 上方
(i-1, j)(向下走一步到当前) - 左方
(i, j-1)(向右走一步到当前)
所以有经典转移方程:
f ( i , j ) = f ( i − 1 , j ) + f ( i , j − 1 ) f(i,j)=f(i-1,j)+f(i,j-1) f(i,j)=f(i−1,j)+f(i,j−1)
边界:
- 第一行只能一直向右走,所以都是
1 - 第一列只能一直向下走,所以都是
1
解法一:暴力递归
原理
从终点反推:
- 到
(i,j)的路径数 = 到(i-1,j)+ 到(i,j-1) - 递归到边界结束
问题
会重复计算大量子问题(例如 (m-2,n-2) 会被多次计算),时间复杂度指数级,LeetCode 会超时。
Java 代码
java
class Solution {
public int uniquePaths(int m, int n) {
return dfs(m - 1, n - 1);
}
private int dfs(int i, int j) {
if (i < 0 || j < 0) return 0;
if (i == 0 || j == 0) return 1;
return dfs(i - 1, j) + dfs(i, j - 1);
}
}
复杂度
- 时间复杂度:
O(2^(m+n))(近似指数级) - 空间复杂度:
O(m+n)(递归栈深度)
解法二:记忆化搜索(递归 + 缓存)
原理
在暴力递归基础上,增加一个 memo[i][j]:
- 若
memo[i][j]已算过,直接返回 - 否则递归计算并存储
避免重复子问题后,复杂度降到多项式级别。
时序图
dfs(i,j) Solution 调用方 dfs(i,j) Solution 调用方 uniquePaths(m,n) dfs(m-1,n-1) dfs(i-1,j) dfs(i,j-1) 写入 memo[i][j] 返回 memo[i][j] 返回结果
Java 代码
java
class Solution {
private int[][] memo;
public int uniquePaths(int m, int n) {
memo = new int[m][n];
return dfs(m - 1, n - 1);
}
private int dfs(int i, int j) {
if (i < 0 || j < 0) return 0;
if (i == 0 || j == 0) return 1;
if (memo[i][j] != 0) return memo[i][j];
memo[i][j] = dfs(i - 1, j) + dfs(i, j - 1);
return memo[i][j];
}
}
复杂度
- 时间复杂度:
O(m*n) - 空间复杂度:
O(m*n)(memo)+O(m+n)(递归栈)
解法三:二维动态规划
原理
递归改迭代,按行或按列填表。
- 创建
dp[m][n] - 初始化第一行、第一列为
1 - 从
(1,1)开始:
dp[i][j] = dp[i-1][j] + dp[i][j-1] - 返回
dp[m-1][n-1]
流程图
否
是
开始
创建 dp[m][n]
第一行全部置 1
第一列全部置 1
双重循环 i=1..m-1, j=1..n-1
dp[i][j]=dp[i-1][j]+dp[i][j-1]
遍历结束?
返回 dp[m-1][n-1]
结束
Java 代码
java
class Solution {
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
for (int i = 0; i < m; i++) dp[i][0] = 1;
for (int j = 0; j < n; j++) dp[0][j] = 1;
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}
}
复杂度
- 时间复杂度:
O(m*n) - 空间复杂度:
O(m*n)
解法四:一维动态规划(空间优化)
原理
观察二维 DP 转移式:
dp[i][j]只依赖当前行左边值dp[i][j-1]- 和上一行同列值
dp[i-1][j]
可以把二维压缩成一维数组 dp[j]:
dp[j]在更新前代表"上一行同列"dp[j-1]在更新后代表"当前行左侧"
转移:
d p [ j ] = d p [ j ] + d p [ j − 1 ] dp[j] = dp[j] + dp[j-1] dp[j]=dp[j]+dp[j−1]
Java 代码
java
class Solution {
public int uniquePaths(int m, int n) {
int[] dp = new int[n];
// 第一行全是1
for (int j = 0; j < n; j++) dp[j] = 1;
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[j] = dp[j] + dp[j - 1];
}
}
return dp[n - 1];
}
}
复杂度
- 时间复杂度:
O(m*n) - 空间复杂度:
O(n)
解法五:数学组合
原理
从起点到终点总共要走:
- 向下
m-1步 - 向右
n-1步
总步数 m+n-2,本质是从这些步中选哪几步向下(或向右):
ans = C ( m + n − 2 , m − 1 ) = C ( m + n − 2 , n − 1 ) \text{ans} = C(m+n-2, m-1) = C(m+n-2, n-1) ans=C(m+n−2,m−1)=C(m+n−2,n−1)
为什么好
- 不需要 DP 表
- 复杂度最低(线性于较小维度)
Java 代码(避免中间溢出)
java
class Solution {
public int uniquePaths(int m, int n) {
int total = m + n - 2;
int k = Math.min(m - 1, n - 1);
long ans = 1;
for (int i = 1; i <= k; i++) {
ans = ans * (total - k + i) / i;
}
return (int) ans;
}
}
复杂度
- 时间复杂度:
O(min(m,n)) - 空间复杂度:
O(1)
各解法对比
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 暴力递归 | 指数级 | O(m+n) |
思路简单,但会超时 |
| 记忆化搜索 | O(m*n) |
O(m*n) |
递归写法友好 |
| 二维 DP | O(m*n) |
O(m*n) |
最经典,容易理解 |
| 一维 DP | O(m*n) |
O(n) |
工程上常用优化 |
| 数学组合 | O(min(m,n)) |
O(1) |
最简洁高效 |
推荐写法
面试/笔试建议优先:
- 二维 DP(解释最直观)
- 一维 DP(展示优化能力)
- 数学组合(展示抽象能力)