01 背包模板
网上讲刷题的文章太多了,K神(Krahets),灵神(灵茶山艾府),卡尔(代码随想录),左神(左程云)这几个大佬讲的都很好,感兴趣的自己去看,本文只是无脑刷题笔记而已。、
背包问题我个人推荐大家去看一下代码随想录写的教程,通俗易懂一文上手。
01背包问题是一个经典的组合优化问题,它可以被描述为:给定一组物品,每个物品有一个重量和一个价值,同时给定一个固定的背包容量,要求在不超过背包容量的前提下,选择一些物品装入背包,使得装入的物品总价值最大化。
问题的名称源于每个物品只有两种选择:要么装入背包(取1),要么不装入背包(取0)。因此,该问题的解可以用一个长度为n的二进制数组表示,其中n是物品的数量。数组的第i个元素表示第i个物品是否被选中。
01背包要点:物品不能重复选,只有"选"或者"不选"。
模板:
py
dp = []*背包容量
# 双层for循环,外层遍历物品,内层逆序遍历背包容量
for i in 物品:
for j in 背包:(逆序遍历)
dp[j] = dp[j-i] + i价值
416. 分割等和子集 - 力扣(LeetCode)
给你一个 只包含正整数 的 非空 数组 nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
ini
输入: nums = [1,5,11,5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
ini
输入: nums = [1,2,3,5]
输出: false
解释: 数组不能分割成两个元素和相等的子集。
提示:
1 <= nums.length <= 200
1 <= nums[i] <= 100
PY
class Solution:
def canPartition(self, nums: List[int]) -> bool:
'''
总和:s
选好的子集和:c
未选的子集和:s-c
s-c = c --> s/2 = c 的时候True
转化为01背包问题,从数中选出和为s/2的数
01背包一定先遍历物品,再**倒序**遍历背包容量
'''
s = sum(nums)
if s%2 == 1:
return False
target = s//2
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
这段代码实现了一个判断给定数组 nums
是否可以分割成两个子集,使得这两个子集的元素和相等(即可以等分为两个和相等的子集)。这是一个典型的0-1背包问题的变种。
代码中的 canPartition
函数使用动态规划的方法来解决这个问题。下面是对代码的分析:
- 计算数组元素总和
s
,如果s
为奇数,直接返回False
,因为奇数无法等分成两个相等的子集。 - 如果
s
为偶数,计算出目标和target
为s // 2
,因为如果存在两个子集的和相等,那么每个子集的和就应该是target
。 - 创建一个长度为
target + 1
的动态规划数组dp
,其中dp[i]
表示是否可以使用给定的数组中的元素凑出和为i
。 - 开始遍历数组
nums
的每个元素,对于每个元素nums[i]
,在动态规划数组dp
中,倒序遍历从target
到nums[i]
(包括nums[i]
)的范围,这是因为在更新dp[j]
时需要用到之前已经计算过的dp[j-nums[i]]
,确保每个元素只能被使用一次。 - 更新
dp[j]
,将其更新为max(dp[j], dp[j-nums[i]] + nums[i])
,表示在考虑当前元素nums[i]
时,是否可以凑出和为j
。这里的max
表示在考虑选取当前元素和不选取当前元素两种情况中选择最大的值。 - 最终,返回
dp[-1] == target
,即判断是否存在一种选取数组中的元素,凑出和为target
,从而分割成两个和相等的子集。
通过动态规划的思想,将原问题转化为一个经典的0-1背包问题,并通过填表的方式逐步求解,最终判断是否可以将数组分割成两个和相等的子集。
1049. 最后一块石头的重量 II - 力扣(LeetCode)
有一堆石头,用整数数组 stones
表示。其中 stones[i]
表示第 i
块石头的重量。
每一回合,从中选出任意两块石头 ,然后将它们一起粉碎。假设石头的重量分别为 x
和 y
,且 x <= y
。那么粉碎的可能结果如下:
- 如果
x == y
,那么两块石头都会被完全粉碎; - 如果
x != y
,那么重量为x
的石头将会完全粉碎,而重量为y
的石头新重量为y-x
。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0
。
示例 1:
ini
输入: stones = [2,7,4,1,8,1]
输出: 1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。
示例 2:
ini
输入: stones = [31,26,33,21,40]
输出: 5
提示:
1 <= stones.length <= 30
1 <= stones[i] <= 100
PY
class Solution:
def lastStoneWeightII(self, stones: List[int]) -> int:
'''
和416分割等和子集是一样的!
能否完全粉碎就是看能否找到两个子集和相等
如果和不相等,返回两个子集最小的差值
'''
s = sum(stones)
# 这里不能用 s%2==0 返回True,因为可能相差2
target = s // 2
n = len(stones)
dp = [0]*(s+1)
for i in range(n):
for j in range(target,stones[i]-1,-1):
dp[j] = max(dp[j],dp[j-stones[i]]+stones[i])
return s-2*dp[target]
在给定一组石头的重量 stones
时,你需要将这些石头分成两堆,使得这两堆的重量尽可能接近。最终返回两堆石头的重量差。
代码中的 lastStoneWeightII
函数同样使用了动态规划的思想来解决这个问题。下面是对代码的分析:
- 计算所有石头的总重量
s
。 - 计算目标和
target
为s // 2
。和之前一样,这是因为问题可以转化为寻找一个子集,使得该子集的和尽可能接近target
。 - 初始化一个长度为
s + 1
的动态规划数组dp
,其中dp[i]
表示是否可以用石头的重量凑出和为i
。 - 开始遍历数组
stones
的每个元素,对于每个元素stones[i]
,在动态规划数组dp
中,倒序遍历从target
到stones[i]
(包括stones[i]
)的范围,这是因为在更新dp[j]
时需要用到之前已经计算过的dp[j-stones[i]]
,确保每个石头只能被使用一次。 - 更新
dp[j]
,将其更新为max(dp[j], dp[j-stones[i]] + stones[i])
,表示在考虑当前石头stones[i]
时,是否可以凑出和为j
。这里的max
表示在考虑选取当前石头和不选取当前石头两种情况中选择最大的值。 - 最终,返回
s - 2 * dp[target]
,这表示将石头分成两堆,一堆的重量是dp[target]
,另一堆的重量就是总重量s
减去第一堆的重量。
这段代码通过动态规划的方式,将原问题转化为一个类似0-1背包问题,找到一个子集的和尽可能接近 target
,然后通过计算差值得出将石头分成两堆后的最小重量差。
494. 目标和 - 力扣(LeetCode)
给你一个非负整数数组 nums
和一个整数 target
。
向数组中的每个整数前添加 '+'
或 '-'
,然后串联起所有整数,可以构造一个 表达式 :
- 例如,
nums = [2, 1]
,可以在2
之前添加'+'
,在1
之前添加'-'
,然后串联起来得到表达式"+2-1"
。
返回可以通过上述方法构造的、运算结果等于 target
的不同 表达式 的数目。
示例 1:
ini
输入: nums = [1,1,1,1,1], target = 3
输出: 5
解释: 一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3
示例 2:
ini
输入: nums = [1], target = 1
输出: 1
提示:
1 <= nums.length <= 20
0 <= nums[i] <= 1000
0 <= sum(nums[i]) <= 1000
-1000 <= target <= 1000
PY
class Solution:
def findTargetSumWays(self, nums, target):
'''
数组和 s
整数和 p
负数和 n = s-p
target = s-n = p-(s-p)
--> target + s = 2p
--> p = (s + target) // 2
--> 01背包
背包容量p
dp[j]表示当前选数之和为j的方案数,
状态转移方程为:dp[j] += dp[j-nums[i]],
表示选当前数nums[i]和不选nums[i]两种情况。
'''
s = sum(nums)
n = len(nums)
if abs(target) > abs(s) or (s + target) % 2 == 1:
return 0
target = (s+target)//2
dp = [0] * (target + 1)
dp[0] = 1
for i in range(n):
for j in range(target, nums[i] - 1, -1):
dp[j] += dp[j - nums[i]]
return dp[-1]
给定一个非空整数数组 nums
和一个目标整数 target
,你需要找出有多少种将数组中的元素通过添加正号或负号相加,可以得到目标整数 target
。
代码中的 findTargetSumWays
函数使用动态规划的方法来解决这个问题。下面是对代码的分析:
- 计算数组元素总和
s
。 - 判断如果目标整数的绝对值大于总和的绝对值,或者
(s + target)
为奇数,直接返回0
,因为无法通过添加正号或负号得到目标整数。 - 根据分析,我们可以得出
target + s = 2p
,其中p
为一组元素前面添加正号的和,这个问题被转化为一个背包问题,寻找和为p
的组合数。 - 创建一个长度为
target + 1
的动态规划数组dp
,其中dp[j]
表示选取一些元素和为j
的方案数。 - 初始化
dp[0]
为1
,表示当不选取任何元素时,和为0
的方案数为1
。 - 开始遍历数组
nums
的每个元素,对于每个元素nums[i]
,在动态规划数组dp
中,倒序遍历从target
到nums[i]
(包括nums[i]
)的范围,这是因为在更新dp[j]
时需要用到之前已经计算过的dp[j-nums[i]]
,确保每个元素只能被使用一次。 - 更新
dp[j]
,将其更新为dp[j] += dp[j - nums[i]]
,表示在考虑选取当前数nums[i]
和不选取当前数nums[i]
两种情况下,和为j
的方案数之和。 - 最终,返回
dp[-1]
,即在给定数组中通过添加正号或负号得到目标整数target
的方案数。
这段代码通过动态规划的思想,将问题转化为0-1背包问题,寻找和为特定值的方案数,然后通过动态规划数组逐步计算得出最终的结果。
474. 一和零 - 力扣(LeetCode)
给你一个二进制字符串数组 strs
和两个整数 m
和 n
。
请你找出并返回 strs
的最大子集的长度,该子集中 最多 有 m
个 0
和 n
个 1
。
如果 x
的所有元素也是 y
的元素,集合 x
是集合 y
的 子集 。
示例 1:
arduino
输入: strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出: 4
解释: 最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。
示例 2:
arduino
输入: strs = ["10", "0", "1"], m = 1, n = 1
输出: 2
解释: 最大的子集是 {"0", "1"} ,所以答案是 2 。
提示:
1 <= strs.length <= 600
1 <= strs[i].length <= 100
strs[i]
仅由'0'
和'1'
组成1 <= m, n <= 100
PY
class Solution:
def findMaxForm(self, strs, m: int, n: int) -> int:
'''
01背包问题
背包容量 0-m 1-n
dp[z][o]表示最多有z个0和o和1的子集的数量
'''
dp = [[0]*(n+1) for _ in range(m+1)]
for s in strs:
o = s.count('1')
z = s.count('0')
for i in range(m,z-1,-1):
for j in range(n,o-1,-1):
dp[i][j] = max(dp[i][j], dp[i-z][j-o] + 1)
return dp[-1][-1]
给定一个由仅含有字符 '0' 和 '1' 的字符串组成的数组 strs
,以及两个非负整数 m
和 n
,表示可以使用的 '0' 和 '1' 的数量上限,你需要从数组中选取字符串,使得选取的字符串的 '0' 和 '1' 的数量分别不超过 m
和 n
,并返回最大的选取字符串数量。
代码中的 findMaxForm
函数使用了动态规划的思想来解决这个问题。下面是对代码的分析:
- 创建一个二维动态规划数组
dp
,其中dp[z][o]
表示最多可以使用z
个 '0' 和o
个 '1' 的子集的数量。 - 对于每个字符串
s
,统计其中 '0' 和 '1' 的数量,分别记为z
和o
。 - 使用嵌套循环遍历背包容量的范围,即从
m
到z
以及从n
到o
。这是因为在计算dp[i][j]
时,需要用到dp[i-z][j-o]
,而为了避免重复计算,需要倒序遍历。 - 更新
dp[i][j]
,将其更新为max(dp[i][j], dp[i-z][j-o] + 1)
,表示在考虑选取当前字符串时,可以将其加入到符合条件的子集中,并计算新的子集数量。 - 最终,返回
dp[-1][-1]
,表示在给定限制条件下,最大的选取字符串数量。
总的来说,这段代码使用动态规划的思想,将问题转化为一个0-1背包问题,通过填表的方式逐步求解,得出最大的选取字符串数量。