416分割等和子集

链接:416. 分割等和子集 - 力扣(LeetCode)

给你一个 只包含正整数非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

示例 1:

输入: nums = 1,5,11,5

输出: true

**解释:**数组可以分割成 1, 5, 511

示例 2:

输入: nums = 1,2,3,5

输出: false

**解释:**数组不能分割成两个元素和相等的子集。

提示:

  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= 100

前置知识:01背包

情景:有n件物品和一个容量为bagweight的背包,第i件物品的重量为weight[i],其价值为value[i],每件物品只能用一次。求装入背包的物品的最大价值总和

暴力解法:每个物品要么被选,要么不被选。使用回溯即可。时间复杂度为O(2^n)

使用动态规划进行优化。先明确dp数组的维度及其含义。由于要考虑物品及其重量,因此先使用二维数组。dp[i][j]中,i表示物品编号,j表示背包容量,dp[i][j]表示使用容量为j的背包,从下标为0-i的物品里任意取,所能得到的最大价值总和

接着,确定递推公式

以推导dp[2][3]为例:

dp[2][3]的意思是:使用容量为3的背包,从下标为0、1、2的物品里任意取,所能得到的最大价值总和。那么要求解它,就有两种情况:放入物品2、不放入物品2

如果不放入物品2,那么这个物品2有没有都不会产生影响,因此dp[2][3]=dp[1][3]

如果放入物品2,那么就必须要给物品2留出足够的空间

如果物品2的重量已经超过了3,那么这个背包是不能放入物品2的。如果没有超过3,假设它的重量是2,那么对于容量为3的背包来说,放入物品2后还剩的容量为1,所以只需要看容量为1的背包从下标为0、1的物品里面取,所能得到的最大价值总和是多少就可以了。此时dp[2][3]=dp[1][1]+value[2]

抽象来看,求解dp[i][j]的过程为:

不放入物品i:物品i没发挥作用,dp[i][j]=dp[i-1][j]

放入物品i:要给它留位置,放入物品i前,背包容量为j-weight[i],因此dp[i][j]=dp[i-1][j-weight[i]]+value[i]

两者取最大值即可

然后看如何初始化dp数组(因为i是从i-1推导出来,因此必须要初始化):

dp[i][0]:背包容量为0,因此不能放入任何物品,所以其值初始化为0

dp[0][j]:只能选择物品0,那么就需要判断j是否不小于weight[0],如果小于就不选择,因为容量不够,其值初始化为0,反之初始化为value[0]

由递推公式可见,其它下标都可以从左上方数值推导出来,因此其它位置的元素默认初始化为0即可

所以,初始化工作实际上只需要对第0行进行处理即可,其它都初始化为0

再看dp数组的遍历顺序:

是先遍历物品还是先遍历背包?其实都可以,因为要么从正上方推导出结果,要么从左上方推导出结果

先遍历物品再遍历背包相当于先定下来列,再一行一行去填

反之,先遍历背包再遍历物品相当于先定下来行,再一列一列去填

滚动数组

从二维数组的递推公式可以看到,dp[i][j]只和dp[i-1][j]以及dp[i-1][j-weight[i]]有关系,而上一行(即i-1)又可以通过滚动数组拷贝,因此考虑优化为一维数组

优化后的递推公式为:dp[j]=max(dp[j-weight[i]]+value[i],dp[j])

理解:括号内的dp[j]是上一行的dp[j],相当于是dp[i-1][j]

接着看如何初始化这个一维数组

dp[j]表示:容量为j的背包所能容纳物品的最大价值,那么dp[0]=0,而其它位置都会由前面位置的值推导出来,因此只需要将每个位置都初始化为0即可

再看遍历顺序:正确做法是先遍历物品再倒序遍历背包容量

首先,为什么要倒序?

如果是正序遍历,同一个物品可能会被放入多次。因为我们使用的是滚动数组,每一次更新dp数组,都是看是否要将物品i加入背包中,所以如果前面有一个容量的背包已经加入了物品i,那么后面的位置再判断是否要将物品i加入背包就已经没有意义了。而倒序遍历就避免了这个问题,因为dp[j]是依赖前面的元素求出来的

举个例子:weight[0]=1,value[0]=20

如果正序遍历,那么在第一轮遍历中,dp[1]=dp[1-weight[0]]+value[0]=20

dp[2]=dp[2-weight[0]]+value[0]=40

dp[2]的含义是:选择物品0,在容量为2的背包中所能容纳的最大价值。很显然只能选择物品0,最大价值应该是20,但这里因为算多了一次,所以变成了40

通过倒序遍历,我们才真正找到了dp[i-1][j-weight[i]],因为要看上一行的元素,因此就不能先覆盖掉它

其次,为什么先物品后背包容量?

如果先遍历背包容量再遍历物品,那么代码应该是这样的:

cpp 复制代码
for(int j=bagweight;j>=1;j--) {
    for(int i=0;i<weight.size();i++) {
        if(j>=weight[i]) dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);
	}
}

假设bagweight=5,有三个物品,重量分别为1、2、3,价值分别为1、2、3:

一开始,j=5,进入内层循环:

i=0,j>=weight[0]=1dp[5]=dp[5-1]+value[0]=0+1=1

i=1,j>=weight[1]=2,dp[5]=max(dp[5],dp[5-2]+2)=2

走到这里我们就发现不对了,明明容量为5的背包是可以同时放入重量分别为1和2的物品的,总价值应该为3,但是i=1时却为2

如果是先遍历物品,再遍历背包容量:

cpp 复制代码
for(int i=0;i<weight.size();i++) {
	for(int j=bagweight;j>=weight[i];j--) {
        dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);
    }
}

还是假设bagweight=5,有三个物品,重量分别为1、2、3,价值分别为1、2、3:

第一轮,i=0,进入内层循环

j=5,dp[5]=dp[4]+1=1

j=4,dp[4]=dp[3]+1=1

以此类推,这一轮除了dp[0]=0之外,其它都为1,因为只能选择物品0,选了肯定比不选的价值高

第二轮,i=1,进入内层循环

j=5,dp[5]=max(1,dp[3]+2)=3

可以看到,这时候背包是真正放入了物品0和物品1

抽象地说,因为必须使用倒序遍历,所以如果是先遍历背包容量再遍历物品,那么由于前面的背包都还没被处理过,所以相当于你每次只能放入一个物品

因此,必须要先遍历物品,再遍历背包容量

回归题目

问题等价于:能否找到一个子序列,使得其和为数组元素和的一半?

也就是说,我们可以先求出数组元素和sum,再判断是否存在子序列使得其和为sum/2

相当于,我们现在有一个容量为sum/2的背包,我们只需要关注:是否存在这么一堆商品能够将这个背包装满?

这里数字就是物品,物品的价值和重量都是数字的值

那么我们还是要求背包所能容纳的最大价值,然后看这个最大价值是否等于背包容量即可

根据前面01背包的知识,我们知道:

  • dp[j]表示容量为j的背包所能容纳的最大价值
  • dp[0] = 0
  • 外层遍历物品,内层遍历背包容量
  • 遍历背包容量时,必须倒序遍历

Java

java 复制代码
class Solution {
    public boolean canPartition(int[] nums) {
        int sum = 0;
        for(int num : nums) {
            sum += num;
        }
        if(sum % 2 == 1) {
            return false;
        }

        int target = sum / 2;

        int[] dp = new int[10005]; //每个元素最大100,最多200个,sum/2最大为10000,因此数组大小设置为10005

        //外层遍历物品,内层倒序遍历背包容量
        for(int i = 0; i < nums.length; i++) {
            for(int j = target; j >= nums[i]; j--) {
                dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
            }
        }

        return dp[target] == target;
    }
}

Python

python 复制代码
class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        n = len(nums)
        if n < 2 :
            return False
        
        total = sum(nums)
        if total % 2 == 1 :
            return False
        
        target = total // 2

        dp = [0 for _ in range(10005)]

        for i, num in enumerate(nums) :
            for j in range(target, num - 1, -1) : 
                dp[j] = max(dp[j], dp[j - num] + num)
        
        return dp[target] == target
相关推荐
Keven_112 小时前
算法札记:Tarjan与拓扑序(Topo)的关系
算法·拓扑·tarjan
qq_452396232 小时前
第三篇:《Jenkins Pipeline as Code:Declarative Pipeline 深度实战》
java·servlet·jenkins
ShallWeL2 小时前
AUC、F1、召回率怎么选
人工智能·算法·机器学习
布朗克1682 小时前
Go 入门到精通-15-错误处理
开发语言·python·golang·错误处理
再让我睡两分钟2 小时前
【无标题】
android·java·数据库·人工智能·prompt·ai应用开发
m0_547486662 小时前
《Python程序设计与数据分析》全套PPT课件
python·数据分析
水龙吟啸2 小时前
华为2026.6.3机考选择题+编程题【速刷敲黑板】
人工智能·深度学习·算法·华为
盼小辉丶2 小时前
OpenCV-Python实战(33)——构建实时目标检测与跟踪系统
python·opencv·目标检测·计算机视觉
学究天人2 小时前
数学公理体系大全:第十四章 向量空间与模:线性代数的公理化与推广
线性代数·算法·矩阵·动态规划·抽象代数