文章目录
1. 01背包理论基础
有n种物品,每种物品只有一个,每个物品有自己的重量和价值,有一个最多只能放重量为m的背包,问装满整个背包的最大价值是多少
1.1 确定dp[i][j]的含义
dp[i][j]
:下标为[0,i]的物品任取,放进容量为j的背包的最大价值为dp[i][j]
i表示的是物品的下标,j表示的是背包最大容量,dp[i][j]表示的是价值
背包容量为j的情况下,考虑前i个物品的最佳组合,得到最大价值
1.2 确定递推公式
dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]]+value[i])
不放物品i 放物品i
解释:选择某个物品的时候只有放或者不放两种选择
解法归纳
一、如果装不下当前物品,那么前n个物品的最佳组合和前n-1个物品的最佳组合是一样的
二、如果装得下当前物品。
假设1:装当前物品,再给当前物品预留相应空间的情况下,前n-1个物品最佳组合加上当前物品的价值就是总价值(此时前n-1个物品的最佳组合是容量减去第n个物品重量的情况下
)
假设2:不装当前物品,那么前n个物品的最佳组合和前n-1个物品的最佳组合是一样的
取max(假设1, 假设2),为当前最佳组合的价值
关键代码:
1.3 实现代码(二维)
方式一:第一行第一列都初始化为0
m, n = map(int, input().split()) # 物品种类, 物品容量
weight = list(map(int, input().split())) # 物品重量
value = list(map(int, input().split())) # 物品价值
dp = [] # 初始化m+1行,n+1列的背包
for i in range(m+1):
temp = [0] * (n+1)
dp.append(temp)
for i in range(1, m): # 第一行和第一列都初始化为0
for j in range(1, n+1):
if j-weight[i] < 0:
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[-1][-1])
方式二:第一列初始为0,第一行根据题目条件动态初始化
m, n = map(int, input().split()) # 物品种类, 物品容量
weight = list(map(int, input().split())) # 物品重量
value = list(map(int, input().split())) # 物品价值
dp = [] # 初始化m行,n+1列的背包
for i in range(m):
temp = [0] * (n+1)
dp.append(temp)
# 初始化第一行
# 含义:前0个物品,背包容量在j的最大价值,此时的j必须大于weight[0],第一个物品重量
for i in range(weight[0], n+1):
dp[0][j] = value[0]
for i in range(1, m):
for j in range(1, n+1):
if j-weight[i-1] < 0: # 这里为什么是i-1呢,`因为weight是从0下标开始的,而i, j都是下标1开始所以减一`
dp[i][j] = dp[i-1][j] # 拷贝上一层的数据
else:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i-1]]+value[i-1])
print(dp[-1][-1])
1.4 理解二维dp压缩为一维dp
由于二维的递推公式为:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i])
我们可以发现,dp[i][j] = max(正上方的值,左上方的值),数据都是由上一层拷贝而来,可以考虑将二维矩阵压缩成一行,每一次根据第一行来更新第二行的信息,然后再把第二行替换为第一行,为了避免覆盖的问题,遍历背包可以从右边往左边遍历。因为如果从左边往右边遍历的话,取左上角的值就会被更新,从右边向左边左上角的值保留的依然是上一层还没有更新的数据
矩阵压缩为:dp[j] = max(dp[j], dp[j-weight[i]]+value[i])
一维dp代码
m, n = map(int, input().split()) # 物品种类, 物品容量
weight = list(map(int, input().split())) # 物品重量
value = list(map(int, input().split())) # 物品价值
dp = [0] * (n+1)
for i in range(m): # 遍历物品
for j in range(n, weight[i]-1, -1): # 遍历背包,从大到小
dp[j] = max(dp[j], dp[j-weight[i]]+value[i])
print(dp[-1])
2.01背包相关题目
2.1 分割等和子集 (考虑能不能装满背包)
题目描述见: 分割等和子集
思路
为什么这里的重量和价值是一样的呢?
首先:想一想01背包的目标:选物品装入背包,让背包的价值最大
因此本题的目标是:让装满背包的价值最大,但是题目描述中没有价值这个概念,如何使得价值最大呢?
不妨物品的单位重量的价值都相等,将数组中的元素视为重量同时也视为价值
因此重量最大就是价值最大
def canPartition(self, nums: List[int]) -> bool:
if sum(nums) % 2 != 0:
return False
bagSize = sum(nums) // 2
# 问题转变为nums数组中的元素,能否装进bagSize大小的背包中
# 确定dp数组的含义;dp[i][j] 前i个物品任取,放到容量为j的背包中得到的最大价值是dp[i][j],这里的重量和价值都一样
# 确定递推公式: dp[i][j] = max(dp[i-1][j], dp[i-1][j-nums[i]]+nums[i])
dp = []
for i in range(len(nums)+1): # 物品
temp = [0] * (bagSize+1) # 容量
dp.append(temp)
for i in range(1, len(nums)+1):
for j in range(1, bagSize+1):
if j - nums[i-1] < 0:
dp[i][j] = dp[i-1][j]
else:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-nums[i-1]]+nums[i-1])
if dp[i][j] == bagSize:
return True
return False
2.2 最后一块石头的重量II (考虑能装多少装多少)
题目描述见:最后一块石头的重量II
思路
分成两堆石头,并且石头的重量尽可能相等,然后相互抵消,以下展示了二维dp的做法
def lastStoneWeightII(self, stones: List[int]):
bagSize = sum(stones) // 2 # 一个背包尽量凑成这么大、
# dp[i][j] 含义前i个元素任取,装进容量为j的背包中得到的最大重量/价值
dp = []
for i in range(len(stones)+1):
temp = [0] * (bagSize+1)
dp.append(temp)
for i in range(1, len(stones)+1):
for j in range(1, bagSize+1):
if j - stones[i-1] < 0:
dp[i][j] = dp[i-1][j]
else:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-stones[i-1]]+stones[i-1])
# 得到的dp[-1][-1]就是左边最大的能装入的容量 右边的重量减去左边的重量
return sum(stones) - dp[-1][-1] - dp[-1][-1]
2.3 目标和(考虑有多少种装进去的方式)
题目描述见:目标和
这里要注意第一行和第一列为什么这样初始化,以及内在得含义(
难点
)
递推公式;放+不放,如果不妨得话容量不会减少,放的话容量就要减少
def findTargetSumWays(self, nums: List[int], target: int):
total = sum(nums)
if total < abs(target) or (total+target) % 2 != 0:
return 0
# 1.确定dp[i][j]含义:前i个物品装入容量为j得背包有几种方法
# 2.确定递推公式: dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]]
bagSize = (total+target) // 2
dp = []
for i in range(len(nums)):
temp = [0] * (bagSize+1)
dp.append(temp)
# 初始化第一行和第一列
dp[0][0] = 1 # 物品0装入背包容量为0得背包只有一种方法,就是不装入
for j in range(1, bagSize+1):
if j == nums[0]:
dp[0][j] = 1
numZero = 0 # 第一列物品0得数量
for i in range(len(nums)):
if nums[i] == 0:
numZero += 1
dp[i][0] = 2 ** numZero
# 递推
for i in range(1, len(nums)):
for j in range(1, bagSize+1):
if j - nums[i] < 0:
dp[i][j] = dp[i-1][j]
else:
dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]]
return dp[-1][-1]
2.4 一和零(装满这个背包最多有多少个物品)
题目描述见:一和零
1.本题以三维的方式展现,对于三维数组初始化的理解,可以参考三维数组初始化
def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
# 1.确定dp数组的含义:dp[i][j][k] 遍历前i个物品,装满j个0和k个1的背包,最多要多少个物品
# 2.确定递推公式: dp[i][j][k] = max(dp[i-1][j][k], dp[i-1][j-x][k-y]+1)
dp = [] # [[[0],[0],[0]]]
# 初始化,初始化len(strs)+1层
for i in range(len(strs)+1):
dp.append([])
# 中间层,初始化m+1行
for j in range(m+1):
dp[i].append([])
dp[i][j] = [0] * (n+1)
for i in range(1, len(strs)+1):
# 计算0和1的个数
zeros = strs[i-1].count('0')
ones = strs[i-1].count('1')
for j in range(m+1):
for k in range(n+1):
if j -zeros < 0 or k-ones < 0:
dp[i][j][k] = dp[i-1][j][k]
else:
dp[i][j][k] = max(dp[i-1][j][k], dp[i-1][j-zeros][k-ones]+1)
return dp[-1][-1][-1]