概述
代码链接:https://leetcode.cn/problems/partition-equal-subset-sum/solution/by-flix-szk7/
视频链接:https://space.bilibili.com/525438321/channel/seriesdetail?sid=672176
0-1背包:有n种物品,每种物品只有一个
完全背包:有n种物品,每种物品有无限个
多重背包:有n种物品,每种物品的个数各不相同
背包问题一般包括两层循环,一层遍历物品,一层遍历背包的容量。二维数组,两层循环顺序可以颠倒,
01背包问题
动态规划是解决「0-1 背包问题」的标准做法。一般地,我们定义: d p [ i ] [ j ] dp[i][j] dp[i][j] 表示前 i i i 件物品放入一个容量为 j j j 的背包可以获得的最大价值,则状态转移过程可表示为:
- 不选择第 i i i 件物品:问题转化为了前 i − 1 i-1 i−1 件物品放入容量为 j j j 的背包中所获得的价值: d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j]=dp[i-1][j] dp[i][j]=dp[i−1][j] ;
- 选择第
i
i
i 件物品:第
i
i
i 件物品占据容量
w
i
w_i
wi,前
i
−
1
i-1
i−1 件物品放入剩下的容量为
j
−
w
i
j-w_i
j−wi 的背包中,问题也就转化为了前
i
−
1
i-1
i−1 件物品放入容量为
j
−
w
i
j-w_i
j−wi 的背包中所获得的价值
d
p
[
i
−
1
]
[
j
−
w
i
]
dp[i-1][j-w_i]
dp[i−1][j−wi] 加上要放入的第
i
i
i 件物品的价值
v
i
v_i
vi
dp[i][j]=dp[i-1][j-w_i]+v_i
。注意,能放入第 i i i 件物品的前提为: w i ≤ j w_i leq j wi≤j。
两种情况取较大者:
d
p
[
i
]
[
j
]
=
max
{
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
−
1
]
[
j
−
w
i
]
+
v
i
}
d p[i][j]=max left{d p[i-1][j], d p[i-1]left[j-w_iright]+v_iright}
dp[i][j]=max{dp[i−1][j],dp[i−1][j−wi]+vi}
求最优解的背包问题中,有的题目要求 恰好装满背包 时的最优解,有的题目则要求 不超过背包容量 时的最优解。一种区别这两种问法的实现方法是在状态初始化的时候有所不同。[摘自@ 《背包问题九讲》 (网页版) (PDF版)]
初始化的 d p dp dp 数组事实上就是在背包中没有放入任何物品时的合法状态:
- 如果要求恰好装满背包,那么在初始化时 d p [ i ] [ 0 ] = 0 dp[i][0]=0 dp[i][0]=0,其它 d p [ i ] [ 1 , 2 , . . . , ∗ ] dp[i][1,2,...,∗] dp[i][1,2,...,∗] 均设为 − ∞ -infty −∞。这是因为此时只有容量为 0 的背包可能被价值为 0 的 nothing “恰好装满”,而其它容量的背包均没有合法的解,属于未定义的状态。
- 如果只是要求不超过背包容量而使得背包中的物品价值尽量大,初始化时应将 d p [ ∗ ] [ ∗ ] dp[∗][∗] dp[∗][∗] 全部设为 0。这是因为对应于任何一个背包,都有一个合法解为 “什么都不装”,价值为 0。
本题题目分析:
对于本题而言, n u m s [ i ] nums[i] nums[i] 则对应于常规背包问题中第 i i i 件物品的重量。我们要做的是判断是否可以将数组 n u m s nums nums 分割成两个子集,使得两个子集的元素和相等,这是一个 T r u e / F a l s e True / False True/False 的存在问题。我们 的目标是从 n u m s nums nums 中选出若干个数字使其和恰好等于数组总和的一半,记为 target = s u m ( n u m s ) 2 text{target}=frac{{sum}(nums)}{2} target=2sum(nums) 。
I. 状态定义
对于本题,定义 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示从前 i i i 个数字中选出若干个,刚好可以使得被选出的数字其重量和为 j j j。
II. 状态转移
根据本题的要求,上述「0-1 背包问题」的状态转移方程 (1) 可修改为:
d p [ i ] [ j ] = d p [ i − 1 ] [ j ] or d p [ i − 1 ] [ j − n u m s [ i − 1 ] ] dp[i][j]=d p[i-1][j] quad text { or } quad d p[i-1][j-n u m s[i-1]] dp[i][j]=dp[i−1][j] or dp[i−1][j−nums[i−1]]
对于 i > 0 i>0 i>0 且 j > 0 j>0 j>0 的情况,如何确定 d p [ i ] [ j ] dp[i][j] dp[i][j] 的值?需要分别考虑以下两种情况。
- 如果
j
≥
n
u
m
s
[
i
]
j geq nums[i]
j≥nums[i],则对于当前的数字
n
u
m
s
[
i
]
nums[i]
nums[i] ,可以选取也可以不选取,两种情况只要有一个为 true,就有
d
p
[
i
]
[
j
]
=
t
r
u
e
dp[i][j]=true
dp[i][j]=true。
- 如果不选取 n u m s [ i ] nums[i] nums[i] ,则 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j]=d p[i-1][j] dp[i][j]=dp[i−1][j] ;
- 如果选取 nums [ i ] [i] [i] ,则 d p [ i ] [ j ] = d p [ i − 1 ] [ j − n u m s [ i ] ] d p[i][j]=dp[i-1][j-nums[i]] dp[i][j]=dp[i−1][j−nums[i]] 。
- 如果 j < n u m s [ i ] j<nums[i] j<nums[i],则在选取的数字的和等于 j j j 的情况下无法选取当前的数字 n u m s [ i ] nums[i] nums[i],因此有 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j]=dp[i-1][j] dp[i][j]=dp[i−1][j]。
状态转移方程如下:
d p [ i ] [ j ] = { d p [ i − 1 ] [ j ] ∣ d p [ i − 1 ] [ j − n u m s [ i ] ] , j ≥ n u m s [ i ] d p [ i − 1 ] [ j ] , j < n u m s [ i ] d p[i][j]= begin{cases}d p[i-1][j] mid d p[i-1][j-n u m s[i]], & j geq n u m s[i] \ d p[i-1][j], & j<n u m s[i]end{cases} dp[i][j]={dp[i−1][j]∣dp[i−1][j−nums[i]],dp[i−1][j],j≥nums[i]j<nums[i]
最终得到 d p [ n − 1 ] [ t a r g e t ] d p[n-1][target] dp[n−1][target] 即为答案。
III. 初始化
记非空数组 nums 的长度为 n n n 。为便于状态更新,减少对边界的判断,初始二维 d p dp dp 数组维度为 ( n + 1 ) × ( t a r g e t + 1 ) (n+1) times(target+1) (n+1)×(target+1) ,其中第一维为 n + 1 n+1 n+1 也意味着:第 i i i 个数字为 n u m s [ i − 1 ] nums[i-1] nums[i−1],第 1 个数字为 n u m s [ 0 ] n u m s[0] nums[0] ,第 0 个数字为空。
初始化时:
- d p [ 0 ] [ 0 ] = d p[0][0]= dp[0][0]= True:表示可以从前 0 个数字中选出若干个数字使得其和为 0 ,即「空集合」不选任何数字即可得到 0 。
- 对于其他 d p [ 0 ] [ j ] , j ≥ 1 dp[0][j], j geq 1 dp[0][j],j≥1 ,则有 d p [ 0 ] [ j ] = d p[0][j]= dp[0][j]= False:「空集合」无法选出任何数字使得其和为 j ≥ 1 j geq 1 j≥1
d p [ i ] [ 0 ] = d p[i][0]= dp[i][0]= True 在程序迭代实现中已有体现,在此无需提前重复定义。
class Solution:
def canPartition(self, nums: List[int]) -> bool:
total = sum(nums)
if total % 2 == 1: # 总和无法等分
return False
target = total // 2 # 最大值大于总和的一半,无法分割
if max(nums) > target:
return False
n = len(nums)
# dp[i][j]: 从前i个元素中选出若干个数字刚好能够组成j
dp = [[False] * (target+1) for _ in range(n)]
dp[0][0] = True # 其他 dp[0][j]均为False
for i in range(1, n): # 一层遍历物品
for j in range(target+1): # 一层遍历背包的容量
if j < nums[i-1]: # 容量有限,无法选择第i个数字nums[i-1]
dp[i][j] = dp[i-1][j]
else: # 可选择第i个数字nums[i-1],也可不选
# dp[i][j] = max(dp[i-1][j-nums[i-1]], dp[i-1][j])
dp[i][j] = dp[i-1][j-nums[i-1]] | dp[i-1][j]
return dp[n-1][target]
最后
以上就是阔达缘分为你收集整理的数据结构【动态规划-0-1背包】| leetcode 416. 分割等和子集(中等)的全部内容,希望文章能够帮你解决数据结构【动态规划-0-1背包】| leetcode 416. 分割等和子集(中等)所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复