主题
入门视频
请看视频 动态规划入门:从记忆化搜索到递推,其中包含把记忆化搜索 1:1 翻译成递推的技巧。
一、启发思考:寻找子问题
假设
我们要解决的问题是从
分类讨论:
- 如果最后一步爬了
个台阶,那么我们得先爬到 ,要解决的问题缩小成:从 爬到 有多少种不同的方法。 - 如果最后一步爬了
个台阶,那么我们得先爬到 ,要解决的问题缩小成:从 爬到 有多少种不同的方法。
由于这两种情况都会把原问题变成一个和原问题相似的、规模更小的子问题,所以可以用递归解决。
注 1:从大往小思考,主要是为了方便把递归翻译成递推。从小往大思考也是可以的。
注 2:动态规划有「选或不选」和「枚举选哪个」两种基本思考方式。在做题时,可根据题目要求,选择适合题目的一种来思考。本题用到的是「枚举选哪个」。
二、递归怎么写:状态定义与状态转移方程
因为要解决的问题都是「从
分类讨论:
- 如果最后一步爬了
个台阶,那么我们得先爬到 ,要解决的问题缩小成:从 爬到 有多少种不同的方法。 - 如果最后一步爬了
个台阶,那么我们得先爬到 ,要解决的问题缩小成:从 爬到 有多少种不同的方法。
由于这两种方法是互相独立的(爬的台阶个数不同),所以根据加法原理,从
递归边界:
递归入口:
问:为什么
答:也可以这样理解,如果
python
# 会超时的递归代码
class Solution:
def climbStairs(self, n: int) -> int:
def dfs(i: int) -> int:
if i <= 1: # 递归边界
return 1
return dfs(i - 1) + dfs(i - 2)
return dfs(n)cpp
// C++ 版待补充cpp
// 会超时的递归代码
class Solution {
int dfs(int i) {
if (i <= 1) { // 递归边界
return 1;
}
return dfs(i - 1) + dfs(i - 2);
}
public:
int climbStairs(int n) {
return dfs(n);
}
};复杂度分析
- 时间复杂度:
。搜索树可以近似为一棵二叉树,树高为 ,所以节点个数为 ,遍历搜索树需要 的时间。 - 空间复杂度:
。递归需要 的栈空间。
三、递归 + 记录返回值 = 记忆化搜索
上面的做法太慢了,怎么优化呢?
注意到「先爬
一叶知秋,整个递归中有大量重复递归调用(递归入参相同)。由于递归函数没有副作用,同样的入参无论计算多少次,算出来的结果都是一样的,因此可以用记忆化搜索来优化:
- 如果一个状态(递归入参)是第一次遇到,那么可以在返回前,把状态及其结果记到一个
数组中。 - 如果一个状态不是第一次遇到(
中保存的结果不等于 的初始值),那么可以直接返回 中保存的结果。
注意:
Python 用户可以无视上面这段,直接用
@cache装饰器。
python
class Solution:
def climbStairs(self, n: int) -> int:
@cache # 缓存装饰器,避免重复计算 dfs 的结果
def dfs(i: int) -> int:
if i <= 1: # 递归边界
return 1
return dfs(i - 1) + dfs(i - 2)
return dfs(n)cpp
// C++ 版待补充cpp
class Solution {
vector<int> memo;
int dfs(int i) {
if (i <= 1) { // 递归边界
return 1;
}
int& res = memo[i]; // 注意这里是引用
if (res) { // 之前计算过
return res;
}
return res = dfs(i - 1) + dfs(i - 2); // 记忆化
}
public:
int climbStairs(int n) {
memo.resize(n + 1);
return dfs(n);
}
};复杂度分析
- 时间复杂度:
。由于每个状态只会计算一次,动态规划的时间复杂度 状态个数 单个状态的计算时间。本题状态个数等于 ,单个状态的计算时间为 ,所以动态规划的时间复杂度为 。 - 空间复杂度:
。有多少个状态, 数组的大小就是多少。
四、1:1 翻译成递推
我们可以去掉递归中的「递」,只保留「归」的部分,即自底向上计算。
具体来说,
相应的递推式(状态转移方程)也和
相当于之前是用递归去计算每个状态,现在是枚举并计算每个状态。
初始值
答案为
python
class Solution:
def climbStairs(self, n: int) -> int:
f = [0] * (n + 1)
f[0] = f[1] = 1
for i in range(2, n + 1):
f[i] = f[i - 1] + f[i - 2]
return f[n]cpp
// C++ 版待补充cpp
class Solution {
public:
int climbStairs(int n) {
vector<int> f(n + 1);
f[0] = f[1] = 1;
for (int i = 2; i <= n; i++) {
f[i] = f[i - 1] + f[i - 2];
}
return f[n];
}
};复杂度分析
- 时间复杂度:
。 - 空间复杂度:
。
五、空间优化
观察状态转移方程,发现一旦算出
这意味着每次循环,只需要知道「上一个状态」和「上上一个状态」的
每次循环,计算出新的状态
- 「上上一个状态」就是
,更新 。 - 「上一个状态」就是
,更新 。
最后答案为
python
class Solution:
def climbStairs(self, n: int) -> int:
f0 = f1 = 1
for _ in range(2, n + 1):
new_f = f1 + f0
f0 = f1
f1 = new_f
return f1cpp
// C++ 版待补充python
class Solution:
def climbStairs(self, n: int) -> int:
f0 = f1 = 1
for _ in range(2, n + 1):
f0, f1 = f1, f1 + f0
return f1cpp
// C++ 版待补充cpp
class Solution {
public:
int climbStairs(int n) {
int f0 = 1, f1 = 1;
for (int i = 2; i <= n; i++) {
int new_f = f1 + f0;
f0 = f1;
f1 = new_f;
}
return f1;
}
};复杂度分析
- 时间复杂度:
。 - 空间复杂度:
。
六、矩阵快速幂优化
把状态转移方程用矩阵乘法表示,即
把上式中的三个矩阵分别记作
那么有
其中
初始值
答案为
python
# a @ b,其中 @ 是矩阵乘法
def mul(a: List[List[int]], b: List[List[int]]) -> List[List[int]]:
return [[sum(x * y for x, y in zip(row, col)) for col in zip(*b)]
for row in a]
# a^n @ f0
def pow_mul(a: List[List[int]], n: int, f0: List[List[int]]) -> List[List[int]]:
res = f0
while n:
if n & 1:
res = mul(a, res)
a = mul(a, a)
n >>= 1
return res
class Solution:
def climbStairs(self, n: int) -> int:
m = [[1, 1], [1, 0]]
f0 = [[1], [0]]
fn = pow_mul(m, n, f0)
return fn[0][0]cpp
// C++ 版待补充python
import numpy as np
class Solution:
def climbStairs(self, n: int) -> int:
m = np.array([[1, 1], [1, 0]], dtype=object)
f0 = np.array([1, 0], dtype=object)
fn = np.linalg.matrix_power(m, n) @ f0
return fn[0]cpp
// C++ 版待补充cpp
using matrix = vector<vector<long long>>;
// 返回矩阵 a 和矩阵 b 相乘的结果
matrix mul(matrix& a, matrix& b) {
int n = a.size(), m = b[0].size();
matrix c = matrix(n, vector<long long>(m));
for (int i = 0; i < n; i++) {
for (int k = 0; k < a[i].size(); k++) {
if (a[i][k] == 0) {
continue;
}
for (int j = 0; j < m; j++) {
c[i][j] += a[i][k] * b[k][j];
}
}
}
return c;
}
// a^n * f0
matrix pow_mul(matrix a, int n, matrix& f0) {
matrix res = f0;
while (n) {
if (n & 1) {
res = mul(a, res);
}
a = mul(a, a);
n >>= 1;
}
return res;
}
class Solution {
public:
int climbStairs(int n) {
matrix m = {
{1, 1},
{1, 0},
};
matrix f0 = {{1}, {0}};
matrix fn = pow_mul(m, n, f0);
return fn[0][0];
}
};复杂度分析
- 时间复杂度:
。 - 空间复杂度:
。
思考题
- 如果每次可以爬
或 或 个台阶呢?空间优化的写法要怎么做? - 如果某些台阶不能爬呢?(输入一个数组表示不能爬的台阶编号)
欢迎在评论区发表你的思路。
分类题单
- 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
- 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
- 单调栈(基础/矩形面积/贡献法/最小字典序)
- 网格图(DFS/BFS/综合应用)
- 位运算(基础/性质/拆位/试填/恒等式/思维)
- 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
- 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
- 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
- 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
- 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
- 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
- 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)
欢迎关注 B站@灵茶山艾府