第 3 章 常用算法
3.4 动态规划算法
3.4.1 概述
动态规划算法与分治法类似,也是通过将原问题拆分为若干子问题,然后递归求解子问题,最后再组合子问题的解进而得到原问题的解。不同的是,分治算法解决的问题,子问题通常相互独立。而动态规划解决的问题,子问题具有重叠现象,所谓的重叠现象,是指不同的子问题会有相同的子子问题。
对于这些重复的子问题,若不加干预,会进行多次的重复计算,效率低下。动态规划意义就是,能够保证每个重复的子问题只计算一次,其解决问题的思路就是将计算过的子问题保存起来,后续计算相同子问题时,便可直接获取结果。
动态规划算法的实现方式分为自上而下的记忆化递归和自下而上的迭代。自上而下的递归方式只需考虑原问题到子问题的递归公式,而无序考虑子问题的执行顺序;而自下而上的迭代方式,除了需要考虑递推关系,还需要确保子问题从小到大依次执行。
3.4.2 案例一:爬楼梯
力扣70题leetcode.cn/problems/cl...
爬有n个台阶的楼梯,每次可以爬1或2个台阶。有多少种不同的方法可以爬到楼顶?
- 方法一
由于每次只能个或个台阶,所以第n个台阶可能是从第n-1个台阶爬1阶上来的,也可能是从第n-2个台阶爬2阶上来的,所以爬到第n阶的方法数就等于爬到第n-1阶的方法数加上爬到第n-2阶的方法数。故可以得到状态转移方程:f(n) = f(n-1) + f(n-2)。
Python
def climb(n):
if n == 1:
return 1
elif n == 2:
return 2
else:
return climb(n - 1) + climb(n - 2)
- 方法二
既然f(n) = f(n-1) + f(n-2),那么我们也可以自下而上从1阶台阶开始逐渐增加,并记录当前阶方法数和当前阶-1的方法数。
Python
def climb(n):
pre = 1
cur = 1
for _ in range(1, n):
pre, cur = cur, pre + cur
return cur
3.4.3 案例二:最大的连续子数组之和
力扣53题leetcode.cn/problems/ma...
找出整数数组nums中数组之和最大的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
-
输入:nums = -2,1,-3,4,-1,2,1,-5,4
-
输出:6
解释:连续子数组 4,-1,2,1 的和最大,为 6 。
1) 思路分析
用f(x)表示以x结尾的最大子数组之和,考虑处于位置i时,有两种选择:
-
与之前的子数组组成连续子数组,此时子数组之和f(i) = f(i-1) + numsi。
-
中断连续,从头开始一个新的连续子数组,此时f(i) = numsi。
得到状态转移方程:f(i) = max( f(i-1) + numsi , numsi )。
持续组成连续子数组,除非连续子数组之和已经<0,此时中断连续。
Python
def max_subarray(nums):
result, f = nums[0], 0
for i in nums:
# 连续子数组之和若小于0,则中断连续
if f < 0:
f = 0
# 累加连续子数组之和
f += i
# 更新最大值
if result < f:
result = f
return result
3.4.4 案例三:0-1背包
0-1背包问题是一个经典的动态规划问题。其基本描述是:给定一组物品,每个物品都有一个重量和一个价值,在背包容量有限的情况下,如何选择物品放入背包,使得背包中物品的总价值最大化,且总重量不超过背包的容量。
-
物品:有n个物品,每个物品i的重量为weighti和价值valuei。
-
背包容量:背包可以承载的最大重量为W。
-
目标:选择若干物品放入背包,使得总重量不超过W,且总价值最大。

1) 方法一
-
定义状态
定义一个二维数组 dpij,表示前i个物品中,总重量不超过j的情况下,能够取得的最大价值。
-
i:表示考虑第i个物品。
-
j:表示背包当前容量为j。
-
-
状态转移
对于每个物品i,有两个选择:

-
实现步骤
-
初始化二维数组dp,i行、W+1列,因为要存放背包容量为0~W时的状态。
-
循环中每次增加一个可选物品。
-
每增加一个可选物品后遍历背包容量为0~W。考虑该物品是否放得下背包,如果放得下,将放进去后和不放进去进行比较。
-



Python
def knapsack(weights, values, W):
n = len(weights)
# 初始化二维数组dp,dp[i][j]表示前i个物品中,背包容量为j时的最大价值
dp = [[0] * (W + 1) for _ in range(n)]
# 每次增加一个可选物品,增加物品后遍历一次背包重量
for i in range(n):
for j in range(1, W + 1):
# 如果当前物品放的进背包,进行比较
if weights[i] <= j:
dp[i][j] = max(dp[i - 1][j], values[i] + dp[i - 1][j - weights[i]])
# 如果当前物品放不进背包,使用上轮相同j的状态
else:
dp[i][j] = dp[i - 1][j]
print(f"前{i + 1}个物品,背包容量为{j}时")
for row in range(len(dp)):
print(dp[row])
return dp[n - 1][W]
if __name__ == "__main__":
weights = [1, 2, 3] # 物品的重量
values = [3, 2, 6] # 物品的价值
W = 3 # 背包的最大容量
print(knapsack(weights, values, W))
2)方法二
可以看到在方法一中,我们每轮遍历时只用到上1轮的结果,与这一轮的结果无关,因此我们只用一维数组存储上1轮的结果即可。
并且遍历时需要从后往前遍历,防止这轮结果将还需要用到的上轮结果覆盖。
并且不用全部遍历,遍历到背包当前重量刚好等于当前物品重量即可。
Python
def knapsack(weights, values, W):
n = len(weights)
dp = [0] * (W + 1)
for i in range(n):
for j in range(W, weights[i] - 1, -1): # 从后往前遍历
dp[j] = max(dp[j], values[i] + dp[j - weights[i]])
return dp[W]
if __name__ == "__main__":
weights = [1, 2, 3] # 物品的重量
values = [3, 2, 6] # 物品的价值
W = 3 # 背包的最大容量
print(knapsack(weights, values, W))
3.4.5 案例四:完全背包
完全背包问题是0-1背包问题的一种扩展。与0-1背包不同,完全背包问题允许每个物品可以被选取多次,也就是说,物品的数量没有限制。
1) 方法一
- 定义状态
定义一个二维数组 dpij,表示前i个物品中,总重量不超过j的情况下,能够取得的最大价值。
-
i:表示考虑第i个物品。
-
j:表示背包当前容量为j。
-
状态转移
相较于0-1背包,仅有选择放入第i个物品时发生了变化。
对于每个物品i,有两个选择:

-
实现步骤
-
初始化二维数组dp,i行、W+1列,因为要存放背包容量为0~W时的状态。
-
循环中每次增加一个可选物品。
-
每增加一个可选物品后遍历背包容量为0~W。考虑该物品是否放得下背包,如果放得下,将放进去后和不放进去进行比较。
-



Python
def knapsack(weights, values, W):
n = len(weights)
# 初始化二维数组dp,dp[i][j]表示前i个物品中,背包容量为j时的最大价值
dp = [[0] * (W + 1) for _ in range(n)]
# 每次增加一个可选物品,增加物品后遍历一次背包重量
for i in range(n):
for j in range(1, W + 1):
# 如果当前物品放的进背包,进行比较
if weights[i] <= j:
dp[i][j] = max(dp[i - 1][j], values[i] + dp[i][j - weights[i]])
# 如果当前物品放不进背包,使用上轮相同j的状态
else:
dp[i][j] = dp[i - 1][j]
print(f"前{i + 1}个物品,背包容量为{j}时")
for row in range(len(dp)):
print(dp[row])
return dp[n - 1][W]
if __name__ == "__main__":
weights = [1, 2, 3] # 物品的重量
values = [3, 7, 11] # 物品的价值
W = 3 # 背包的最大容量
print(knapsack(weights, values, W))
2)方法二
方法一同样可以优化为一维数组。可以看到在方法一中我们既使用了上1轮结果,也使用了本轮i之前的结果,但是两者上下没有重叠。
这时我们遍历时需要从前向后遍历,因为可能会用到本轮i之前的结果。

Python
def knapsack(weights, values, W):
n = len(weights)
dp = [0] * (W + 1)
for i in range(n):
for j in range(weights[i], W + 1):
dp[j] = max(dp[j], dp[j - weights[i]] + values[i])
return dp[W]
if __name__ == "__main__":
weights = [1, 2, 3] # 物品的重量
values = [3, 7, 11] # 物品的价值
W = 3 # 背包的最大容量
print(knapsack(weights, values, W))