01背包和完全背包就够用了
def test_2_wei_bag_problem1(weight, value, bagweight):
# 二维数组
dp = [[0] * (bagweight + 1) for _ in range(len(weight))]
# 初始化
for j in range(weight[0], bagweight + 1):
dp[0][j] = value[0]
# weight数组的大小就是物品个数
for i in range(1, len(weight)): # 遍历物品
for j in range(bagweight + 1): # 遍历背包容量
if j < weight[i]:
dp[i][j] = dp[i - 1][j]
else:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
return dp[len(weight) - 1][bagweight]
if __name__ == "__main__":
weight = [1, 3, 4]
value = [15, 20, 30]
bagweight = 4
result = test_2_wei_bag_problem1(weight, value, bagweight)
print(result)
对于背包问题其实状态都是可以压缩的。
在使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);
与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,只用dp[j](一维数组,也可以理解是一个滚动数组)。
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次!
背包问题是计算机科学和优化领域中的经典问题,主要关于资源的最优分配。最著名的背包问题是 0-1 背包问题,但它有很多变种。
0-1 背包问题:
给定 n 个物品,每个物品有一个重量 w[i] 和一个价值 v[i]。现在有一个背包,最多可以承载重量 W。问如何选择物品放入背包,使得背包中物品的总价值最大,同时不超过背包的重量限制?
解决这个问题的一个常见方法是使用动态规划。以下是一个简化的算法:
初始化一个二维数组 dp[n+1][W+1]
其中 dp[i][j] 表示考虑前 i 个物品,总重量不超过 j 的最大价值
for i from 1 to n:
for j from 1 to W:
if w[i-1] <= j:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i-1]] + v[i-1])
else:
dp[i][j] = dp[i-1][j]
返回 dp[n][W],这是最大的价值
这种方法的时间复杂度是 O(nW)。
除了 0-1 背包问题,还有一些其他变种,例如:
分数背包问题:物品可以被分割。这意味着你可以取走物品的一部分而不是整个物品。这个问题可以使用贪婪算法来解决。
完全背包问题:每种物品可以取无数次。这需要稍微修改上面的动态规划解法。
多重背包问题:每种物品有有限的数量。
这些问题虽然看起来很相似,但它们的解决方法可以根据问题的具体限制和要求有所不同。
此问题可以转化为 0-1 背包问题的变种来解决。
思路:
total/2
,其中 total
是数组的总和。使用动态规划来解决这个问题:
target = total/2
。target + 1
的布尔数组 dp
,其中 dp[i]
表示能否从数组中选择部分数字使其和为 i
。dp[0] = true
。num
和从 target
到 1
的每个数字 i
,如果 i >= num
并且 dp[i-num] == true
,则设置 dp[i] = true
。dp[target]
。代码:
def canPartition(nums):
total = sum(nums)
# 如果总和是奇数,则不能将其分为两个和相等的子集
if total % 2 != 0:
return False
target = total // 2
dp = [False] * (target + 1)
dp[0] = True
for num in nums:
for i in range(target, num-1, -1):
dp[i] = dp[i] or dp[i-num]
return dp[target]
# 测试
print(canPartition([1,5,11,5])) # True
print(canPartition([1,2,3,5])) # False
这个算法的时间复杂度为 O(n * target),其中 n 是 nums
的长度,target 是目标和。
只有确定了如下四点,才能把01背包问题套到本题上来。
动规五部曲分析如下:
// 开始 01背包
for(int i = 0; i < nums.size(); i++) {
for(int j = target; j >= nums[i]; j--) { // 每一个元素一定是不可重复放入,所以从大到小遍历
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
class Solution:
def canPartition(self, nums: List[int]) -> bool:
if sum(nums) % 2 != 0:
return False
target = sum(nums) // 2
dp = [0] * (target + 1)
for num in nums:
for j in range(target, num-1, -1):
dp[j] = max(dp[j], dp[j-num] + num)
return dp[-1] == target
这是自己第一次比较系统地学习背包问题,感觉如何识别出一个题目是背包问题是此类题目的重点(笔记末尾总结了如何识别出一个题目是背包问题)
背包问题是优化问题中的一个经典范例,主要围绕有限的资源(如背包的容量)来选择最优解(如最大价值)。虽然背包问题有很多变体,但它们之间存在一些共同特点。以下是一些提示来帮助你识别一个题目是否可能是背包问题:
有限资源:问题中通常会有一个有限的资源,如背包的容量、时间、预算等。
选择的问题:你需要从给定的一组物品或任务中选择一部分,而不是所有。
最优化目标:通常,目标是最大化或最小化某些值,如最大化价值或最小化花费。
重复子问题:解决问题的方法经常涉及到处理重复的子问题。例如,在0-1背包问题中,你可能会多次询问“如果我加入这个物品,剩余的背包容量是多少?”这种结构通常暗示可以使用动态规划来解决。
子集与组合:背包问题经常要求找到一个物品的子集或组合,这样总的权重或花费不超过给定的限制。
隐含的顺序不重要性:在大多数背包问题中,物品或任务的顺序并不重要。这与其他一些问题(如序列对齐或最长上升子序列)形成对比,其中元素的顺序非常重要。
变体提示:
上下文线索:题目描述中可能会有一些关键词或短语,如“最大化总价值”、“容量限制”、“选择子集”等。
不是所有具有上述特点的问题都是背包问题,但上述线索可以帮助你在遇到可能的背包问题时提供一些方向。如果一个问题看起来像背包问题,可以尝试使用背包问题的方法来解决它。如果这种方法有效,那么很可能就是一个背包问题。