主题
一、寻找子问题

看上图,假设左上角的坐标是
想一想,最后一步发生了什么?
- 如果从
向下走到终点 ,那么要解决的问题是从起点 走到 的路径数。 - 如果从
向右走到终点 ,那么要解决的问题是从起点 走到 的路径数。
这些问题都是和原问题相似的、规模更小的子问题,可以用递归解决。
二、状态定义与状态转移方程
根据上面的讨论,定义状态为
讨论我们是如何到达
- 如果是从
过来,那么问题变成从起点 走到 的路径数,即 。 - 如果是从
过来,那么问题变成从起点 走到 的路径数,即 。
这两种情况互斥,根据加法原理,有
递归边界:
。无法从 到达这些位置。 。起点到它自己有一条路径,即原地不动。
递归入口:
三、递归搜索 + 保存递归返回值 = 记忆化搜索
考虑到整个递归过程中有大量重复递归调用(递归入参相同)。由于递归函数没有副作用,同样的入参无论计算多少次,算出来的结果都是一样的,因此可以用记忆化搜索来优化:
- 如果一个状态(递归入参)是第一次遇到,那么可以在返回前,把状态及其结果记到一个
数组中。 - 如果一个状态不是第一次遇到(
中保存的结果不等于 的初始值),那么可以直接返回 中保存的结果。
注意:
Python 用户可以无视上面这段,直接用
@cache装饰器。
具体请看视频讲解 动态规划入门:从记忆化搜索到递推,其中包含把记忆化搜索 1:1 翻译成递推的技巧。
python
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
@cache # 缓存装饰器,避免重复计算 dfs 的结果(一行代码实现记忆化)
def dfs(i: int, j: int) -> int:
if i < 0 or j < 0:
return 0
if i == 0 and j == 0:
return 1
return dfs(i - 1, j) + dfs(i, j - 1)
return dfs(m - 1, n - 1)cpp
// C++ 版待补充python
class Solution:
@cache
def uniquePaths(self, m: int, n: int) -> int:
if m == 0 or n == 0:
return 0
if m == 1 and n == 1:
return 1
return self.uniquePaths(m - 1, n) + self.uniquePaths(m, n - 1)cpp
// C++ 版待补充cpp
class Solution {
public:
int uniquePaths(int m, int n) {
vector memo(m, vector<int>(n));
auto dfs = [&](this auto&& dfs, int i, int j) -> int {
if (i < 0 || j < 0) {
return 0;
}
if (i == 0 && j == 0) {
return 1;
}
int& res = memo[i][j]; // 注意这里是引用
if (res) { // 之前计算过
return res;
}
return res = dfs(i - 1, j) + dfs(i, j - 1);
};
return dfs(m - 1, n - 1);
}
};复杂度分析
- 时间复杂度:
。由于每个状态只会计算一次,动态规划的时间复杂度 状态个数 单个状态的计算时间。本题状态个数等于 ,单个状态的计算时间为 ,所以总的时间复杂度为 。 - 空间复杂度:
。保存多少状态,就需要多少空间。
四、1:1 翻译成递推
我们可以去掉递归中的「递」,只保留「归」的部分,即自底向上计算。
具体来说,
相应的递推式(状态转移方程)也和
初始值:
,翻译自递归边界 。 ,翻译自递归边界 。
答案为
python
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
f = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(m):
for j in range(n):
if i == j == 0:
f[1][1] = 1
else:
f[i + 1][j + 1] = f[i][j + 1] + f[i + 1][j]
return f[m][n]cpp
// C++ 版待补充cpp
class Solution {
public:
int uniquePaths(int m, int n) {
vector f(m + 1, vector<int>(n + 1));
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (i == 0 && j == 0) {
f[1][1] = 1;
} else {
f[i + 1][j + 1] = f[i][j + 1] + f[i + 1][j];
}
}
}
return f[m][n];
}
};也可以把
python
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
f = [[0] * (n + 1) for _ in range(m + 1)]
f[0][1] = 1
for i in range(m):
for j in range(n):
f[i + 1][j + 1] = f[i][j + 1] + f[i + 1][j]
return f[m][n]cpp
// C++ 版待补充cpp
class Solution {
public:
int uniquePaths(int m, int n) {
vector f(m + 1, vector<int>(n + 1));
f[0][1] = 1;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
f[i + 1][j + 1] = f[i][j + 1] + f[i + 1][j];
}
}
return f[m][n];
}
};复杂度分析
- 时间复杂度:
。 - 空间复杂度:
。
五、空间优化
回顾上面的代码,我们在一行一行地计算
算到
能不能只用一个长为
好比在一面墙上画画,原来这面墙画的是
在这个「覆盖」的过程中,对于这面墙的其中一个点
怎么覆盖?看转移方程
python
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
f = [0] * (n + 1)
f[1] = 1
for _ in range(m):
for j in range(n):
f[j + 1] += f[j]
return f[n]cpp
// C++ 版待补充cpp
class Solution {
public:
int uniquePaths(int m, int n) {
vector<int> f(n + 1);
f[1] = 1;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
f[j + 1] += f[j];
}
}
return f[n];
}
};复杂度分析
- 时间复杂度:
。 - 空间复杂度:
。
六、另一种方法:组合数学

再来看示例 1,我们会往下走
比如其中一种走法为
这相当于有
确定哪里填「下」,其余位置必然填「右」。
所以只需要计算填「下」的方案数,也就是从
一般地,我们会往下走
从
组合数的计算方法
本题保证答案小于等于
组合数的计算公式为
把
对比上面两个等式,可以得到如下递推式
由于等式左边
例如
可以先计算
注:也可以这样理解,由于任意连续
个数中必然有 的倍数,所以上述计算过程均为整除,不会产生小数。
所以可以写一个简单的循环,计算
如果
减少循环次数。
python
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
return comb(m + n - 2, m - 1)cpp
// C++ 版待补充python
def comb(n: int, k: int) -> int:
k = min(k, n - k)
res = 1
for i in range(1, k + 1):
res = res * (n + 1 - i) // i
return res
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
return comb(m + n - 2, m - 1)cpp
// C++ 版待补充cpp
class Solution {
long long comb(int n, int k) {
k = min(k, n - k);
long long res = 1;
for (int i = 1; i <= k; i++) {
res = res * (n + 1 - i) / i;
}
return res;
}
public:
int uniquePaths(int m, int n) {
return comb(m + n - 2, m - 1);
}
};复杂度分析
- 时间复杂度:
。 - 空间复杂度:
。
进阶问题
如果网格图中有障碍物,要怎么做?
这题是 63. 不同路径 II。
专题训练
- 动态规划题单的「二、网格图 DP」。
- 数学题单的「§2.2 组合计数」。
分类题单
- 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
- 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
- 单调栈(基础/矩形面积/贡献法/最小字典序)
- 网格图(DFS/BFS/综合应用)
- 位运算(基础/性质/拆位/试填/恒等式/思维)
- 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
- 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
- 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
- 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
- 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
- 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
- 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)
欢迎关注 B站@灵茶山艾府