背包问题
有n件物品和一个最多能背重量为w的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。01背包是每件物品只能用一次;完全背包是每件物品数量无限。求解目标是将哪些物品装入背包里物品价值总和最大。
01背包问题-二维DP数组解法
遍历顺序的选择:只要正序遍历背包容量即可,遍历物品、背包容量的顺序可以颠倒
def test_2_wei_bag_problem1(weight, value, bagweight):
# dp[i][j]代表背包空间为j的情况下,从下标为0~i的物品里面任意取,能达到的最大价值
dp = [[0] * (bagweight + 1) for _ in range(len(weight))]
# 初始化
for j in range(weight[0], bagweight + 1):
dp[0][j] = value[0]
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)
01背包问题-一维DP数组解法
二维DP数组递归公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]),可以看出第i层可以由第i-1层得出
遍历顺序的选择:需要倒序遍历背包容量(为了保证每个物品只添加1次),且需要先遍历物品,再遍历背包容量
def test_1_wei_bag_problem(weight, value, bagWeight):
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])
return dp[bagWeight]
if __name__ == "__main__":
weight = [1, 3, 4]
value = [15, 20, 30]
bagweight = 4
result = test_1_wei_bag_problem(weight, value, bagweight)
print(result)
完全背包问题-一维DP数组解法
遍历顺序的选择:完全背包相对01背包,把倒序遍历背包容量改成正序遍历即可(为了保证每个物品可以添加多次),且完全背包遍历物品、遍历背包容量可以颠倒顺序
def test_CompletePack(weight, value, bagWeight):
dp = [0] * (bagWeight + 1)
for i in range(len(weight)): # 遍历物品
for j in range(weight[i], bagWeight + 1): # 正序遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
return dp[bagWeight]
if __name__ == "__main__":
weight = [1, 3, 4]
value = [15, 20, 30]
bagWeight = 4
result = test_CompletePack(weight, value, bagWeight)
print(result)
01背包问题
LeetCode416题 分割等和子集
可看成01背包问题,判断是否能装满空间为sum(nums) // 2的背包,nums元素既代表价值、又代表重量
class Solution:
def canPartition(self, nums: List[int]) -> bool:
s = sum(nums)
if s % 2 == 1:
return False
target = s // 2
# dp[i][j]代表0-i索引的物品、在背包容量为j时,能装的最大价值
# dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - nums[i]] + nums[i])
# dp数组可压缩为1维
dp = [0] * (target + 1)
for i in range(len(nums)):
for j in range(target, nums[i] - 1, -1):
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])
return dp[-1] == target
LeetCode494题 目标和
可看成装满空间为(sum(nums) + abs(targets)) // 2的背包有多少种方法
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
s = sum(nums)
x = s + abs(target)
if s < abs(target) or x % 2 == 1:
return 0
# dp[i][j]表示nums[0: i+1]中任取若干数,满足和为j的方法数,可优化为一唯dp数组
# 取的数量就代表正号数量
dp = [0] * (x // 2 + 1)
# 当目标和为0时,只有一种方案,即什么都不选,代表都为负号情形
dp[0] = 1
for i in range(len(nums)):
for j in range(x // 2, nums[i] - 1, -1):
dp[j] += dp[j - nums[i]]
return dp[x // 2]
LeetCode474题 一和零
可看成空间为二维情况下的01背包问题,所以dp数组需要是二维
class Solution:
def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
# dp[i][j][k]表示strs[0: i+1]最大子集的长度,该子集中最多有j个0、k个1
# dp[i][j][k] = max(dp[i - 1][j][k], dp[i - 1][j - n0][k - n1] + 1),可优化为二维数组
dp = [[0] * (n + 1) for _ in range(m + 1)]
for s in strs:
n0 = s.count('0')
n1 = len(s) - n0
for i in range(m, n0 - 1, -1):
for j in range(n, n1 - 1, -1):
dp[i][j] = max(dp[i][j], dp[i - n0][j - n1] + 1)
return dp[m][n]
完全背包问题
LeetCode518题 零钱兑换II
装满背包的方法数(组合数,物品间无顺序),先遍历物品,再遍历背包容量
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
dp = [0] * (amount + 1)
dp[0] = 1
for i in range(len(coins)):
for j in range(coins[i], amount + 1):
dp[j] += dp[j - coins[i]]
return dp[-1]
LeetCode377题 组合总和IV
装满背包的方法数(排列数,物品间有顺序),先遍历背包容量,后遍历物品
class Solution:
def combinationSum4(self, nums: List[int], target: int) -> int:
dp = [0] * (target + 1)
dp[0] = 1
for j in range(target + 1):
for i in range(len(nums)):
if j >= nums[i]:
dp[j] += dp[j - nums[i]]
return dp[-1]
LeetCode139题 单词拆分
本质在问,能否装满背包(排列数,物品间有顺序),所以先遍历背包容量,后遍历物品
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
# dp[i][j]代表能否用wordDict[0: i + 1]拼接出s[:j]
# dp[i][j] = dp[i - 1][j] or (dp[i - 1][j - len(wordDict[i])] and s[j - len(wordDict[i]):j] == wordDict[i])
# 本题属于计算排列数,所以要先遍历背包,再遍历物品
dp = [False] * (len(s) + 1)
dp[0] = True
for j in range(1, len(s) + 1):
for i in range(len(wordDict)):
if j >= len(wordDict[i]) and s[j - len(wordDict[i]):j] == wordDict[i]:
dp[j] = dp[j] or dp[j - len(wordDict[i])]
return dp[-1]
LeetCode322题 零钱兑换
本质在问,装满背包用的最少物品数量是多少
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
# dp[i][j]代表coins[0:i + 1]凑成j的最少硬币个数
# dp[i][j] = min(dp[i - 1][j], dp[i - 1][j - coins[i]] + 1)
dp = [10001] * (amount + 1)
dp[0] = 0
for i in range(len(coins)):
for j in range(coins[i], amount + 1):
dp[j] = min(dp[j], dp[j - coins[i]] + 1)
if dp[-1] == 10001:
return -1
return dp[-1]