动态规划 DP

动态规划(Dynamic programming)通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法,常常适用于有重叠子问题和最优子结构性质的问题。

动态规划的经典问题——爬楼梯问题:

给定一个共有 n 阶的楼梯,你每步可以上 1 阶或者 2 阶,请问有多少种方案可以爬到楼顶?

回溯法:将爬楼梯想象为一个多轮选择的过程:从地面出发,每轮选择上 1 阶或 2 阶,每当到达楼梯顶部时就将方案数量加 1 ,当越过楼梯顶部时就将其剪枝。代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def backtrack(choices: list[int], state: int, n: int, res: list[int]) -> int:
"""回溯"""
# 当爬到第 n 阶时,方案数量加 1
if state == n:
res[0] += 1
# 遍历所有选择
for choice in choices:
# 剪枝:不允许越过第 n 阶
if state + choice > n:
continue
# 尝试:做出选择,更新状态
backtrack(choices, state + choice, n, res)
# 回退

def climbing_stairs_backtrack(n: int) -> int:
"""爬楼梯:回溯"""
choices = [1, 2] # 可选择向上爬 1 阶或 2 阶
state = 0 # 从第 0 阶开始爬
res = [0] # 使用 res[0] 记录方案数量
backtrack(choices, state, n, res)
return res[0]

暴力搜索法

爬到第 i-1 阶的方案数加上爬到第 i-2 阶的方案数就等于爬到第 i 阶的方案数! dp[i] = dp[i − 1] + dp[i − 2] 原问题的解可以由子问题的解构建得来,方案数量递推关系如下:

递推关系

代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
def dfs(i: int) -> int:
"""搜索"""
# 已知 dp[1] 和 dp[2] ,返回之
if i == 1 or i == 2:
return i
# dp[i] = dp[i-1] + dp[i-2]
count = dfs(i - 1) + dfs(i - 2)
return count

def climbing_stairs_dfs(n: int) -> int:
"""爬楼梯:搜索"""
return dfs(n)

下图展示了暴力搜索形成的递归树。对于问题 dp[n] ,其递归树的深度为 n,时间复杂度为 O(2^n)。指数阶属于爆炸式增长,如果我们输入一个比较大的 n,则会陷入漫长的等待之中。

0-1背包问题