有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大
暴力法:
每一件物品其实只有两个状态,取或者不取,所以可以使用回溯法搜索出所有的情况,那么时间复杂度就是 o ( 2 n ) o(2^n) o(2n),这里的n表示物品数量。
所以暴力的解法是指数级别的时间复杂度。进而才需要动态规划的解法来进行优化!
用以下例子进行分析
1. 确定dp数组以及下标的含义
对于背包问题,有一种写法, 是使用二维数组,即dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是dp[i][j]
如下图所示:
2. 确定递推公式
再回顾一下dp[i][j]的含义:从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
那么可以有两个方向推出来dp[i][j],
所以递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
3. dp数组初始化
首先从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。如图:
状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。
dp[0][j],即:i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值
那么很明显当 j < weight[0]的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。
当j >= weight[0]时,dp[0][j] 应该是value[0],因为背包容量放足够放编号0物品。
此时dp数组初始化情况如图所示:
dp[0][j] 和 dp[i][0] 都已经初始化了,那么其他下标应该初始化多少呢?
其实从递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出dp[i][j] 是由左上方数值推导出来了,那么 其他下标初始为什么数值都可以,因为都会被覆盖。
初始-1,初始-2,初始100,都可以
但只不过一开始就统一把dp数组统一初始为0,更方便一些。
如图:
4. 确定遍历顺序
根据上图可以看出,是有两个遍历的维度:物品与背包重量
那么应该先遍历哪一个维度呢?
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
递归公式中可以看出dp[i][j]是靠dp[i-1][j]和dp[i - 1][j - weight[i]]推导出来的。
dp[i-1][j]和dp[i - 1][j - weight[i]] 都在dp[i][j]的左上角方向(包括正上方向),那么先遍历物品,再遍历背包的过程如图所示:
再来看看先遍历背包,再遍历物品呢,如图:
虽然两个for循环遍历的次序不同,但是dp[i][j]所需要的数据就是左上角,根本不影响dp[i][j]公式的推导!
但先遍历物品再遍历背包这个顺序更好理解。
5.举例推导dp数组
来看一下对应的dp数组的数值,如图:
代码:
def test_2_wei_bag_problem1():
weight = [1, 3, 4]
value = [15, 20, 30]
bagweight = 4
# 二维数组
dp = [[0] * (bagweight + 1) for _ in range(len(weight))]
# 初始化
for j in range(weight[0], bagweight + 1): #重量小于weight[0]的背包放不进物品0,价值为0,能放进物品0的背包价值为value[0]
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])
print(dp[len(weight) - 1][bagweight])
test_2_wei_bag_problem1()
在使用二维数组的时候,递推公式: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](一维数组,也可以理解是一个滚动数组)。
1. 确定dp数组以及下标的含义
在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。
2.一维dp数组的递推公式
dp[j]为 容量为j的背包所背的最大价值,那么如何推导dp[j]呢?
dp[j]可以通过dp[j - weight[i]]推导出来,dp[j - weight[i]]表示容量为j - weight[i]的背包所背的最大价值。
dp[j - weight[i]] + value[i] 表示 容量为 j - 物品i重量 的背包 加上 物品i的价值。(也就是容量为j的背包,放入物品i了之后的价值即:dp[j])
此时dp[j]有两个选择,一个是取自己dp[j] 相当于 二维dp数组中的dp[i-1][j],即不放物品i,一个是取dp[j - weight[i]] + value[i],即放物品i,指定是取最大的,毕竟是求最大价值,
所以递归公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
可以看出相对于二维dp数组的写法,就是把dp[i][j]中i的维度去掉了。
3.一维dp数组如何初始化
关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱。
dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j],那么dp[0]就应该是0,因为背包容量为0所背的物品的最大价值就是0。
那么dp数组除了下标0的位置,初始为0,其他下标应该初始化多少呢?
看一下递归公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
dp数组在推导的时候一定是取价值最大的数,如果题目给的价值都是正整数那么非0下标都初始化为0就可以了。
这样才能让dp数组在递归公式的过程中取的最大的价值,而不是被初始值覆盖了。
那么我假设物品价值都是大于0的,所以dp数组初始化的时候,都初始为0就可以了。
4.一维dp数组遍历顺序
结论:先遍历物品,再遍历背包,且背包重量要从大到小,倒序遍历
问题1:为什么背包重量要倒序遍历?
列表后面的值需要通过与上一层遍历得到的前面的值比较确定
一维数组本质是保留的上层数据,然后计算本层的背包价值,如果正序遍历,就会使需要用到的上层数据值被刷新,导致计算错误
举例1:
假设目前有背包容量为10,可以装的最大价值, 记为dp(10)
即将进来的物品重量为6。价值为9。
那么此时可以选择装该物品或者不装该物品。
这时候如果是正序遍历会怎么样? dp[10] = dp[4] + 9 ,这个式子里的dp[4]就不再是上一层的了,因为你是正序啊,dp[4] 比dp[10]提前更新,那么此时程序已经没法读取到上一层的dp[4]了,当前的dp[4]已经被新更新的下一层的dp[4]覆盖掉了
举例2:
物品0的重量weight[0] = 1,价值value[0] = 15
如果正序遍历
dp[1] = dp[1 - weight[0]] + value[0] = 15
dp[2] = dp[2 - weight[0]] + value[0] = 30 (dp[2 - weight[0]]=dp[1]是前面一行计算的结果15)
此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。
为什么倒序遍历,就可以保证物品只放入一次呢?
倒序就是先算dp[2]
dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp[2 - weight[0]]=dp[1]是初始化的0)
dp[1] = dp[1 - weight[0]] + value[0] = 15
所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。
那么为什么二维dp数组历的时候不用倒序呢?
因为对于二维dp,dp[i][j]都是通过上一层即dp[i - 1][j]计算而来,本层的dp[i][j]并不会被覆盖!
问题2,能不能先遍历背包再遍历物品?
5.举例推导dp数组
一维dp,分别用物品0,物品1,物品2 来遍历背包,最终得到结果如下:
代码:
def test_1_wei_bag_problem():
weight = [1, 3, 4]
value = [15, 20, 30]
bagWeight = 4
# 初始化
dp = [0] * (bagWeight + 1)
for i in range(len(weight)): # 遍历物品
for j in range(bagWeight, weight[i] - 1, -1): # 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
print(dp[bagWeight])
test_1_wei_bag_problem()
思路1:
使用深度优先搜索,找出所有可能的组合,再从这些组合中找出满足条件的结果,条件为:物品总重量小于等于背包重量
weight = [1, 3, 4]
value = [15, 20, 30]
bagweight = 4
ans = 0
def dfs(startindex,total,v):
global ans
if total > bagweight: # 物品重量超过了背包重量,返回
return
if total <= bagweight: # 物品重量小于等于背包重量,计算最大价值
ans = max(ans,v)
for i in range(startindex,len(weight)):
v += value[i] # 物品价值和
total += weight[i] # 物品重量和
dfs(i+1,total,v) # 递归,进入下一层,选第二个元素
v -= value[i] # 回溯,恢复之前加的价值
total -= weight[i] # 回溯,恢复之前加的重量
dfs(0,0,0)
print(ans)
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
- 输入:nums = [1,5,11,5]
- 输出:true
- 解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
- 输入:nums = [1,2,3,5]
- 输出:false
- 解释:数组不能分割成两个元素和相等的子集。
提示:
- 1 <= nums.length <= 200
- 1 <= nums[i] <= 100
题目链接/文章讲解:
https://programmercarl.com/0416.%E5%88%86%E5%89%B2%E7%AD%89%E5%92%8C%E5%AD%90%E9%9B%86.html
视频讲解:https://www.bilibili.com/video/BV1rt4y1N7jE
思路1:
使用深度优先搜索,递归去找恰好使target等于0的情况
nums = [1,5,11,5]
def dfs(startindex,target):
if target == 0: # 终止条件,当目标值为0,代表可以分割出来
return True
if target < 0: # 终止条件,当目标值小于0,代表组合元素和超过了一半,无法分割出来
return False
for i in range(startindex,len(nums)): # 遍历数组
if dfs(i+1,target-nums[i]): # 递归调用
return True
return False
def fun():
total = sum(nums)
if total%2 != 0:
return False
target = total// 2 # 总和的一般
if nums[-1] > target: # nums的最大值大于总和的一半,那么就没法分割
return False
return dfs(0,target)
print(fun())
思路2:
本题要求集合里能否出现总和为 sum / 2 的子集,对应到01背包
nums = [1,5,11,5]
def fun():
total = sum(nums)
if total%2 != 0: # 总和为奇数,不能分割,直接返回False
return False
target = total// 2 # 总和的一半
if max(nums) > target: # nums的最大值大于总和的一半,那么就没法分割
return False
dp = [0]*(target+1)
for i in range(len(nums)):
# j指的是背包总量,倒序遍历,从最大装target开始,遍历到能装下nums[i],再往前就装不下了
for j in range(target,nums[i]-1,-1):
dp[j] = max(dp[j],dp[j-nums[i]]+nums[i])
print(dp)
return dp[target] == target
print(fun())
'''
dp[j]表示 背包总容量(所能装的总重量)是j,放进物品后,背的最大重量为dp[j](也是最大价值,本题最大价值和最大重量相等)。
target为背包目标重量,放进物品dp[target]为最大重量,当最大重量刚好等于target时代表背包装满了,小于target表示还有空余的重量,没装满
'''