一、目标和(Leetcode 494)
假设加法的总和为x,那么减法对应的总和就是sum - x。
所以我们要求的是 x - (sum - x) = target
x = (target + sum) / 2
此时问题就转化为,用nums装满容量为x的背包,有几种方法。
1.确定dp数组以及下标的含义:装满容量为 j 的背包,有dp[ j ] 种方法。
2. 确定递推公式:dp[j] = dp[j] + dp[j - nums[i]]
,即:dp[j] += dp[j - nums[i]]
3. dp数组如何初始化:dp[ 0 ] = 1
4.遍历物品放在外循环,遍历背包在内循环,且内循环倒序(为了保证物品只使用一次)。
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
total = sum(nums) # 计算nums数组中所有元素的总和
# 如果目标值的绝对值大于数组的总和,或者 (target + total) 是奇数,则没有有效方案
if abs(target) > total or (target + total) % 2 == 1:
return 0 # 没有可行方案
# 计算目标背包容量,转换成子集和问题
bagsize = (target + total) // 2 # 目标和的背包容量
# 动态规划数组dp,dp[i]表示和为i的方案数
dp = [0] * (bagsize + 1) # 初始化dp数组,大小为bagsize+1
dp[0] = 1 # 当目标和为0时,只有一种方案:不选择任何数字
# 遍历nums数组中的每个数字
for num in nums:
# 从bagsize到num的倒序遍历,避免重复计算
for j in range(bagsize, num - 1, -1):
# 更新dp[j],即累加当前数字num参与的组合方式
dp[j] += dp[j - num]
# 返回dp[bagsize],即达到目标和bagsize的方案数
return dp[bagsize]
二、一和零(Leetcode 474)
1.确定dp数组以及下标的含义:dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]。
2.确定递推公式:
dp[i][j] 可以由前一个strs里的字符串推导出来,strs里的字符串有zeroNum个0,oneNum个1。
dp[i][j] 就可以是 dp[i - zeroNum][j - oneNum] + 1。
然后我们在遍历的过程中,取dp[i][j]的最大值。
所以递推公式:dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
字符串的zeroNum和oneNum相当于物品的重量(weight[i]),字符串本身的个数相当于物品的价值(value[i])。
这就是一个典型的01背包! 只不过物品的重量有了两个维度而已。
3.dp数组如何初始化:
因为物品价值不会是负数,初始为0,保证递推的时候dp[i][j]不会被初始值覆盖。
4.遍历顺序:外层for循环遍历物品,内层for循环遍历背包容量且从后向前遍历
class Solution:
def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
# 创建一个二维动态规划数组 dp,dp[i][j] 表示在容量 i 个 0 和 j 个 1 时,能够形成的最大子集数量
dp = [[0] * (n + 1) for _ in range(m + 1)] # 初始化二维数组,所有值都为0
# 遍历所有的字符串
for s in strs:
zeroNum = s.count('0') # 统计当前字符串中0的个数
oneNum = len(s) - zeroNum # 统计当前字符串中1的个数
# 从背包容量 m 和 n 开始倒序遍历,防止重复计算
for i in range(m, zeroNum - 1, -1): # 遍历0的容量
for j in range(n, oneNum - 1, -1): # 遍历1的容量
# 状态转移方程:选择当前字符串后,更新dp数组
dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1)
# 最终结果是 dp[m][n],即在容量 m 和 n 下能选择的最大字符串数量
return dp[m][n]
三、完全背包理论基础(Kamacoder 52)
1. 确定dp数组以及下标的含义:
dp[i][j] 表示从下标为[0-i]的物品,每个物品可以取无限次,放进容量为j的背包,价值总和最大是多少。
2. 确定递推公式:
-
不放物品i:背包容量为j,里面不放物品i的最大价值是dp[i - 1][j]。
-
放物品i:背包空出物品i的容量后,背包容量为j - weight[i],dp[i][j - weight[i]] 为背包容量为j - weight[i]且不放物品i的最大价值,那么dp[i][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值
递推公式: dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);
(注意,完全背包二维dp数组 和 01背包二维dp数组 递推公式的区别,01背包中是 dp[i - 1][j - weight[i]] + value[i])
)
3. dp数组如何初始化:
从状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);
可以看出 i 是由 i-1 推导出来。
dp[0][j],即:存放编号0的物品的时候,各个容量的背包所能存放的最大价值。
当 j < weight[0]
的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。
当j >= weight[0]
时,dp[0][j] 如果能放下weight[0]的话,就一直装,每一种物品有无限个。
4. 确定遍历顺序:
01背包二维DP数组,先遍历物品还是先遍历背包都是可以的。
因为两种遍历顺序,对于二维dp数组来说,递推公式所需要的值,二维dp数组里对应的位置都有。
对于完全背包既可以先遍历物品再遍历背包,也可以先遍历背包再遍历物品
def knapsack(n, bag_weight, weight, value):
# 初始化二维DP数组,dp[i][j]表示使用前i种物品(索引0到i-1),容量为j时能获得的最大价值
dp = [[0] * (bag_weight + 1) for _ in range(n)]
# 初始化第一行(i=0,仅考虑第一种物品)
# 对于容量j >= weight[0],可以多次放入第一种物品,价值为(j // weight[0]) * value[0]
for j in range(weight[0], bag_weight + 1):
dp[0][j] = (j // weight[0]) * value[0] # 计算能放入第一种物品的最大次数
# 动态规划主循环
for i in range(1, n):
# 遍历所有可能的背包容量,从0到bag_weight
for j in range(bag_weight + 1):
# 如果当前物品的重量大于当前容量j,无法放入该物品
# 直接继承上一行的结果(不选当前物品)
if j < weight[i]:
dp[i][j] = dp[i - 1][j]
# 否则,选择以下两种情况的最大值:
# 1. 不选当前物品:dp[i-1][j]
# 2. 选当前物品(可重复):dp[i][j - weight[i]] + value[i]
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i])
# 返回使用所有物品且背包容量为bag_weight时能获得的最大价值
return dp[n - 1][bag_weight]
# 读取输入:物品种类数量n和背包容量bag_weight
n, bag_weight = map(int, input().split())
# 初始化存储物品重量和价值的列表
weight = []
value = []
# 读取n行输入,每行包含一种物品的重量和价值
for _ in range(n):
w, v = map(int, input().split())
weight.append(w) # 将物品重量添加到weight列表
value.append(v) # 将物品价值添加到value列表
# 调用knapsack函数并输出最大价值
print(knapsack(n, bag_weight, weight, value))