动态规划(Dynamic Programming,DP)知识点详解及用法
动态规划(DP)是一种解决最优化问题的方法,尤其适用于那些可以分解为子问题并且具有重叠子问题和最优子结构性质的问题。它通过将问题分解为子问题来减少重复计算,并使用记忆化或表格法来存储中间结果,从而提高计算效率。
动态规划的基本要素
- 最优子结构: 问题的最优解可以由子问题的最优解推导出来。也就是说,整个问题的最优解是由各个子问题的最优解组合而成的。
- 重叠子问题: 动态规划适用于那些有重复子问题的场景。也就是说,问题可以被分解为许多子问题,而这些子问题是重复出现的。例如,计算 Fibonacci 数列中的第
n项时,会多次计算相同的子问题。 - 状态转移方程: 动态规划的核心是定义状态(通常是一个数组或矩阵)来保存子问题的解,并通过状态转移方程来推导出当前问题的解。状态转移方程可以用来计算当前状态基于之前的状态。
动态规划的解题步骤
- 定义状态: 需要明确问题的状态是如何表示的,通常用一个数组(或二维数组、矩阵)来存储子问题的结果。
- 初始化状态: 根据问题的边界条件来初始化一些初始状态值,通常这些值是问题的最小解或基础解。
- 状态转移: 根据问题的性质编写状态转移方程,描述如何通过已知的子问题的解来计算当前问题的解。
- 计算顺序: 根据问题的要求选择合适的顺序来计算每个状态。通常会选择从小到大计算(例如,背包问题通常是从预算
0到V),以避免重复计算。 - 返回结果: 通过状态数组的最终结果来得到问题的最优解。
背包问题及其动态规划解法
背包问题是动态规划中的经典问题之一,尤其适用于解决一系列最优化问题。背包问题可以分为0/1背包问题和完全背包问题,分别解决不同场景下的最优化问题。
0/1 背包问题
在0/1背包问题中,我们有 n 个物品,每个物品有两个属性:重量 w[i] 和价值 v[i]。背包的容量为 W,目标是选择若干物品放入背包,使得它们的总重量不超过背包的容量 W,且总价值最大。
状态定义:
dp[j]:表示在容量为j时,背包可以容纳的最大价值。
状态转移方程:
dp[j]=max(dp[j],dp[j−w[i]]+v[i])dp[j] = \max(dp[j], dp[j - w[i]] + v[i])dp[j]=max(dp[j],dp[j−w[i]]+v[i])
- 如果选择物品
i,则需要计算背包容量减少后的最大价值。 - 否则,保留当前容量
j的最大价值。
初始化:
dp[0] = 0,即容量为0时,背包内没有物品,价值为0。
复杂度:
- 时间复杂度为
O(n * W),其中n是物品的数量,W是背包的容量。
完全背包问题
完全背包问题与0/1背包问题类似,不同之处在于每个物品的数量是无限的。即每种物品可以选择多次,直到容量限制被达到。
状态定义:
dp[j]:表示在容量为j时,背包可以容纳的最大价值。
状态转移方程:
dp[j]=max(dp[j],dp[j−w[i]]+v[i])dp[j] = \max(dp[j], dp[j - w[i]] + v[i])dp[j]=max(dp[j],dp[j−w[i]]+v[i])
- 和0/1背包问题类似,但是每次物品可以被多次选择。
初始化:
dp[0] = 0,即容量为0时,背包内没有物品,价值为0。
复杂度:
- 时间复杂度为
O(n * W),其中n是物品的数量,W是背包的容量。
动态规划在此题中的应用
在此问题中,我们实际上面临的是一个完全背包问题,每种货船的数量是有限的,但是我们可以选择多艘船,每艘船的租赁价格和载货量都不同。我们的目标是在给定预算 V 下,最大化货船的载货量。
1. 状态定义
我们用 dp[j] 表示在预算为 j 时,能够租用的货船的最大载货量。
2. 状态转移
对于每种货船,我们可以选择0到该货船的最大数量(m[i])。因此,状态转移方程是:
dp[j]=max(dp[j],dp[j−k⋅v[i]]+k⋅w[i])dp[j] = \max(dp[j], dp[j - k \cdot v[i]] + k \cdot w[i])dp[j]=max(dp[j],dp[j−k⋅v[i]]+k⋅w[i])
k是选择的货船数量(从 1 到m[i])。v[i]是每艘船的租赁成本,w[i]是每艘船的载货量。
3. 初始化
初始化时,dp[0] = 0,表示没有预算时,载货量为 0。
4. 计算顺序
由于每艘船的数量有限,因此我们需要按从大到小的顺序遍历 dp 数组,以避免重复计算某一艘船。
代码实现
python
复制代码
def solution(Q, V, ships):
# dp[j]表示在预算j元情况下,最大载货量
dp = [0] * (V + 1)
# 遍历每种货船
for m, cost, weight in ships:
# 对于每种货船,我们从预算V向下遍历,避免重复使用同一艘船
for j in range(V, -1, -1):
# 尝试从0到m艘船进行选择
for k in range(1, m + 1):
# 如果当前预算足够选择k艘船
if j >= k * cost:
dp[j] = max(dp[j], dp[j - k * cost] + k * weight)
else:
break
# 最终答案是dp[V],即在最大预算V下,能获取的最大载货量
return dp[V]
# 测试用例
if __name__ == "__main__":
ships = [[2, 3, 2], [3, 2, 10]]
ships2 = [[30, 141, 47], [9, 258, 12], [81, 149, 13], [91, 236, 6], [27, 163, 74], [34, 13, 58], [61, 162, 1], [80, 238, 29], [36, 264, 28], [36, 250, 2], [70, 214, 31], [39, 116, 39], [83, 287, 4], [61, 269, 94], [23, 187, 46], [78, 33, 29], [46, 151, 2], [71, 249, 1], [67, 76, 85], [72, 239, 17], [61, 256, 49], [48, 216, 73], [39, 49, 74]]
print(solution(2, 10, ships) == 32) # 期望输出 32
print(solution(23, 400, ships2) == 1740) # 期望输出 1740
总结
通过这道题目,我们掌握了动态规划在完全背包问题中的应用。其核心在于理解状态转移方程和如何有效管理状态更新,尤其是在物品数量有限时,如何避免重复计算。在实际应用中,动态规划方法通常可以显著降低时间复杂度,解决许多优化问题。