理解递推式(或动态规划转移方程)是解决动态规划问题的关键。如果你对这类问题不太理解,下面我将通过一个简化的例子和逐步解释来帮助你理解如何构建和使用递推式。
例子:0/1 背包问题
问题描述
给定一个容量为 W
的背包和 n
个物品,每个物品有一个重量 w[i]
和一个价值 v[i]
。求如何选择物品放入背包,使得总价值最大。
动态规划基本思想
-
状态定义:
dp[i][j]
表示前i
个物品中选择若干放入容量为j
的背包中的最大价值。
-
状态转移:
- 对于第
i
个物品,有两种选择:放入背包或不放入背包。 - 如果不放入背包,状态转移为
dp[i][j] = dp[i-1][j]
。 - 如果放入背包,状态转移为
dp[i][j] = dp[i-1][j-w[i-1]] + v[i-1]
(前提是j
的容量要大于等于w[i-1]
)。
- 对于第
-
初始状态:
- 如果没有物品或背包容量为0,则
dp[0][j] = 0
对于所有j
。
- 如果没有物品或背包容量为0,则
-
目标:
- 求
dp[n][W]
,即前n
个物品在容量为W
的背包中的最大价值。
- 求
代码实现
def knapsack(W, weights, values):
n = len(weights)
dp = [[0] * (W + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for w in range(W + 1):
if w >= weights[i-1]:
dp[i][w] = max(dp[i-1][w], dp[i-1][w-weights[i-1]] + values[i-1])
else:
dp[i][w] = dp[i-1][w]
return dp[n][W]
# 示例输入
W = 50
weights = [10, 20, 30]
values = [60, 100, 120]
# 调用函数
print(knapsack(W, weights, values)) # 输出 220
逐步解释
-
定义状态:
dp[i][j]
:考虑前i
个物品,放入容量为j
的背包中的最大价值。
-
状态转移:
- 不选第
i
个物品:dp[i][j] = dp[i-1][j]
- 选第
i
个物品:dp[i][j] = dp[i-1][j-w[i-1]] + v[i-1]
(如果容量允许)
- 不选第
-
初始化:
dp[0][j] = 0
,因为没有物品时,背包中的价值为 0。
-
求解:
- 通过双重循环,逐步更新
dp
数组,最终dp[n][W]
就是所求的最大值。
- 通过双重循环,逐步更新
更复杂的例子:带附件的背包问题
带附件的背包问题在普通背包问题的基础上增加了更多的组合关系。对于这种问题,我们可以用类似的方法来理解和构建递推式。
-
状态定义:
dp[i][j]
表示前i
个主件及其附件在容量为j
的背包中的最大价值。
-
状态转移:
- 类似于普通背包问题,但需要考虑主件和附件的组合关系。
-
初始化:
dp[0][j] = 0
,没有主件和附件时,背包中的价值为 0。
-
求解:
- 通过双重循环,逐步更新
dp
数组,最终dp[m][n]
就是所求的最大值。
- 通过双重循环,逐步更新
代码实现(带附件的背包问题)
基于你提供的代码,可以进行逐步讲解和理解:
n, m = map(int, input().split()) # 读取背包容量和物品总数
primary, annex = {}, {} # 用于存储主件和附件
# 读取物品信息并分类存储
for i in range(1, m + 1):
x, y, z = map(int, input().split())
if z == 0:
primary[i] = [x, y]
else:
if z in annex:
annex[z].append([x, y])
else:
annex[z] = [[x, y]]
# 主件的个数
m = len(primary)
# 初始化动态规划表
dp = [[0] * (n + 1) for _ in range(m + 1)]
# 用于存储主件及其附件组合的价格和价值
w, v = [[]], [[]]
# 遍历所有主件
for key in primary:
w_temp, v_temp = [], []
w_temp.append(primary[key][0]) # 仅主件
v_temp.append(primary[key][0] * primary[key][1])
if key in annex: # 如果有附件
w_temp.append(w_temp[0] + annex[key][0][0]) # 主件 + 附件1
v_temp.append(v_temp[0] + annex[key][0][0] * annex[key][0][1])
if len(annex[key]) > 1: # 如果有第二个附件
w_temp.append(w_temp[0] + annex[key][1][0]) # 主件 + 附件2
v_temp.append(v_temp[0] + annex[key][1][0] * annex[key][1][1])
w_temp.append(w_temp[0] + annex[key][0][0] + annex[key][1][0]) # 主件 + 附件1 + 附件2
v_temp.append(v_temp[0] + annex[key][0][0] * annex[key][0][1] + annex[key][1][0] * annex[key][1][1])
w.append(w_temp)
v.append(v_temp)
# 动态规划求解
for i in range(1, m + 1):
for j in range(10, n + 1, 10):
max_i = dp[i - 1][j]
for k in range(len(w[i])):
if j - w[i][k] >= 0:
max_i = max(max_i, dp[i - 1][j - w[i][k]] + v[i][k])
dp[i][j] = max_i
# 输出结果
print(dp[m][n])
总结
- 明确状态定义:清楚地定义
dp[i][j]
的含义。 - 找出状态转移方程:根据问题的特点,找出递推关系。
- 初始化状态:设定初始条件。
- 迭代计算:通过循环或递归计算出结果。
- 读取结果:最终结果通常是
dp
表中的某个值。
通过练习和不断总结,你会逐渐掌握构建递推式和解决动态规划问题的方法。
要理解和构建递推式如 dp[i][j] = dp[i-1][j-w[i-1]] + v[i-1]
这样的动态规划转移方程,并不是靠灵光一现的“神仙想法”,而是通过系统的思考和不断练习来掌握的。以下是一些步骤和方法,可以帮助你逐步培养这种思维方式:
1. 理解问题
首先,明确问题的具体要求和限制条件。例如,在背包问题中,目标是最大化价值,而限制条件是背包容量。
2. 定义状态
确定什么是“状态”,即问题在每一步的具体情况。例如:
dp[i][j]
表示前i
个物品中,放入容量为j
的背包的最大价值。
3. 基本思路
在构建递推式时,考虑每一步的选择:
- 如果当前物品不放入背包,那么
dp[i][j]
就等于前i-1
个物品在容量为j
时的最大价值,即dp[i][j] = dp[i-1][j]
。 - 如果当前物品放入背包,那么要考虑容量
j
减去当前物品的重量后,前i-1
个物品的最大价值,再加上当前物品的价值。
4. 状态转移方程
结合基本思路,状态转移方程自然就出来了:
[ dp[i][j] = \max(dp[i-1][j], dp[i-1][j-w[i-1]] + v[i-1]) ]
这表示我们要在不选当前物品和选当前物品两种情况中选择一个最大值。
5. 初始化
确定初始状态,比如在没有物品或背包容量为零的情况下,价值都是零:
[ dp[0][j] = 0 ]
6. 目标
确定最终需要的结果,通常是 dp
表中的某个值,比如 dp[n][W]
,表示考虑所有物品,在容量为 W
的背包中的最大价值。
7. 举例推导
通过举例和手动推导小规模实例,验证状态转移方程是否正确。例如:
- 有 3 个物品,重量和价值分别为
(10, 60)
,(20, 100)
,(30, 120)
,背包容量为 50。
用表格列出每一步的 dp
值,逐步计算,直观理解状态转移。
8. 练习更多问题
解决更多不同类型的动态规划问题,培养动态规划思维:
- 最长公共子序列
- 最小路径和
- 硬币找零
- 戳气球等
示例讲解
用上述步骤具体演示一个例子:0/1 背包问题
示例问题
- 背包容量:50
- 物品:3 个
- 重量:10, 20, 30
- 价值:60, 100, 120
初始化
n = 3 # 物品个数
W = 50 # 背包容量
weights = [10, 20, 30]
values = [60, 100, 120]
dp = [[0] * (W + 1) for _ in range(n + 1)] # 初始化 DP 表
状态转移方程
for i in range(1, n + 1):
for j in range(W + 1):
if j >= weights[i-1]:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-weights[i-1]] + values[i-1])
else:
dp[i][j] = dp[i-1][j]
结果
print(dp[n][W]) # 输出结果:220
手动推导
考虑第 1
个物品(重量 10,价值 60):
- 背包容量
0-9
:无法放入,dp[1][j] = 0
- 背包容量
10-50
:放入后,dp[1][j] = 60
考虑第 2
个物品(重量 20,价值 100):
- 背包容量
0-19
:无法放入,dp[2][j] = dp[1][j]
- 背包容量
20-50
:放入后,dp[2][j] = max(dp[1][j], dp[1][j-20] + 100)
最终:
考虑第 3
个物品(重量 30,价值 120):
- 背包容量
0-29
:无法放入,dp[3][j] = dp[2][j]
- 背包容量
30-50
:放入后,dp[3][j] = max(dp[2][j], dp[2][j-30] + 120)
这样逐步推导,验证递推式的正确性。
总结
掌握动态规划问题的递推式构建,不是靠突发奇想,而是通过理解问题、定义状态、构建状态转移方程、验证和练习来逐步培养的。希望通过这些方法和步骤,你能逐步掌握和运用动态规划解决问题。
标签:背包,temp,annex,问题,key,物品,浅析,dp From: https://blog.csdn.net/hackermengzhi/article/details/139348079