问题背景
有N件物品和⼀个最多能背重量为W 的背包。第i件物品的重量是weight[i],价值是value[i] 。每件物品只能⽤⼀次,求解将哪些物品装⼊背包⾥所得物品价值总和最⼤。
二维dp分析
1.确定dp(dp table)数组及其下标的含义
dp[i][j]:在下标为[0,i]的物品任意选取,放进容量为j的背包中,所的物品的最大价值为dp[i][j]。
2.确定递推公式
我们从第0件物品开始遍历,逐个确定要不要将第i件物品放入背包,这当然要综合考虑物品的重量以及价值。
假设此时我们遍历到第i件物品,此时有两种情况。
第一种:此时背包容量j<第i件物品的重量,这种情况很简单,因为第i件物品肯定不能放进来,那么dp[i][j]=dp[i-1][j]。如果对这个递推公式不是很理解,应该好好看一看dp数组的含义。dp[i-1][j]表示,在下标为[0,i-1]的物品任意选取,放进容量为j的背包中,所的物品的最大价值为dp[i][j-1],此时我们继续遍历第i件物品,而第i件物品的重量weight[i]>j,第i件物品的重量超过了背包容量本身,那么肯定不能将第i件物品放入背包,那么此时dp[i][j]显然等于dp[i-1][j]。
第二种:此时背包容量j>=第i件物品的重量。这种情况就有些复杂了,因为此时我们由两种选择。一种是不把第i件物品放入背包,显然此时dp[i][j]=dp[i-1][j];另一种是把第i件物品放到背包里,显然此时dp[i][j]=dp[i-1][j-weight[i]]+value[i]。这种情况不是很好理解,我一开始也没有理解,看了很多些资料才明白。
我们可以这样想,背包的容量为j,但是在遍历[0,i-1]件物品时,我们并不确定我们到底放进背包了几件物品,也就是说当我们遍历到第i件物品时,背包的容量可能已经小于j,因为背包中可能已经放了其他物品,而我们遍历时比较的是背包的总容量j是否大于或者等于第i件物品的重量weight[i],如果大于或者等于,我们就要考虑要不要将第i件物品放入背包。这时如果我们想要将第i件物品放入背包,那么我们至少要在背包中留出weight[i]的质量空间,以确保我们能够将第i件物品放入背包,如果此时背包中因为在[0,i-1]件物品的遍历过程中放入其他物品导致背包容量小于weight[i]那么我们就要考虑清出一部分物品保证背包剩余的空间能够装下第i件物品即dp[i-1][j-weight[i]],这就表示遍历前i-1件物品时,在背包容量为j-weight[i]的前提下,背包中物品的最大价值为dp[i-1][j-weight[i]]。相信讲到这里大家应该理解为什么dp[i][j]=dp[i-1][j-weight[i]]+value[i]。
分析完第二种情况的两种选择可以得出:dp[i][j]=Math.max(dp[i-1][j],dp[i][j-weight[i]]+value[i])。
综合以上两种情况,递推公式为:
if(j>=weight[i]){
dp[i][j]=Math.max(dp[i-1][j],dp[i][j-weight[i]]+value[i]);
}else{
dp[i][j]=dp[i-1][j];
}
3.dp数组初始化
由递推公式可以得出我们只需要知道dp[0][j]的值即可,其他数据就可以依次递推得出。
for(int j=weight[0];j<=bagWeight;j++){
dp[0][j]=value[0];
}
4.确定遍历的顺序
大家可能首先会思考一个问题,是先遍历物品还是先遍历背包。其实都是可以的(前提:使用二维dp数组)。其实遍历的目的无非就是想要通过最初的状态逐步递推出最终的状态,我们只要保证在递推的过程中我们能够获得所需要的数据(之前状态的数据)即可。如果能理解这段话,那么接下来理解滚动数组就会简单很多。
5.代码实现
public int bagTest(int[] weight,int[] value,int bagWeight){
int n=weight.length;
int[][] dp=new int[n][bagWeight+1];
for(int j=weight[0];j<=bagWeight;j++){
dp[0][j]=value[0];
}
for(int i=1;i<n;i++){
for(int j=0;j<=bagWeight;j++){
if(j>=weight[i]){
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
}else{
dp[i][j]=dp[i-1][j];
}
}
}
return dp[n-1][bagWeight];
}
一维dp分析(滚动数组)
由前面的分析可以知道如果使用二维dp数组,则递推公式为:
if(j>=weight[i]){
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
}else{
dp[i][j]=dp[i-1][j];
}
不难发现,当我们遍历到第i个物品时即dp[i][j],我们所需要的数据仅仅是dp[i-1][j]和dp[i-1][j-weight[i]],而这两个数据都在第i-1层,也就是说我们并不关心第i-2,i-3等之前层次的数据,那么我们可不可以把二维dp数组压缩为一维dp数组呢?只保留第i-1层的数据,供遍历到第i个物品时使用?答案是可以的,接下来我将详细为大家分析如何使用一维dp数组完成01背包问题。
1.确定dp(dp table)数组及其下标的含义
dp[j]:容量为j的背包,所背的物品的最大价值为dp[j]。
2.确定递推公式
先给出递推公式:dp[j]=Math.max(dp[j],dp[j-weight[i]]+value[i]);
为什么递推公式是这样的?我们首先要明白我们为什么可以将二维dp数组压缩为一维dp数组,因为我们发现我们遍历到第i个物品时,我们只需要第i-1层相关的数据,也就是说我们永远只需要相对于当前层的上一层的数据,那么我们为什么不能利用一个一维dp数组保存上一层的数据,然后在遍历的过程中不断刷新一维dp数组的值,保证其相对于当前遍历的物品永远是其上一层的数据。
所以遍历到第i个物品时,dp[j]其实相当于dp[i-1][j],dp[j-weight[i]]+value[i]其实相当于dp[i-1][j-weight[i]]+value[i]。所以遍历到第i个物品时,如果选择不放入第i个物品,那么dp[j]=dp[j],如果选择放入第i个物品那么dp[i]=dp[j-weight[i]]+value[i]。所以我们只需要选择这两种可能的最大的一个,即dp[j]=Math.max(dp[j],dp[j-weight[i]]+value[i])。
3.dp数组初始化
关于初始化,⼀定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱。
我们在回顾一下dp[j]的含义:容量为j的背包,所背的物品的最大价值为dp[j]。
那么的dp[0]:背包容量为0时背包所背的物品的最大价值为dp[0]。显然dp[0]=0。
再来回顾一下递推公式:dp[j]=Math.max(dp[j],dp[j-weight[i]]+value[i])。显然我们要求dp[j]就要依赖于dp[j-weight[i]],这里要注意一下,j-weight[i]>=0,因为背包的容量不可能为负数。而我们从第0件物品开始遍历,0的上一层显然不存在,所以其他下标也初始化为0即可。如果我们从第1件物品开始遍历,那么初始化应该如下:
for(int j=weight[0];j<=bagWeight;j++){
dp[j]=value[0];
}
4.确定遍历的顺序
代码如下:
for(int i=0;i<weight.length;i++){ //遍历物品
for(int j=bagWeight;j>=weight[i];j--){ //遍历背包容量
dp[j]=Math.max(dp[j],dp[j-weight[i]]+value[i]);
}
}
观察上面的代码我们可以发现和二维dp数组遍历相比遍历背包容量时是从bagWeight到weight[i]来遍历的,即倒叙遍历,那我们为甚么不从0到bagWeight正序遍历呢?大家请看接下来的讲解。
对于递归公式:dp[j]=Math.max(dp[j],dp[j-weight[i]]+value[i])。dp[j]作为一个滚动数组,当我们正序遍历背包容量时,假设此时j=5,我们遍历过后更新了dp[5]的值,继续比哪里,假设现在已经遍历到j=8,此时j-weight[i]=5,我们正好需要dp[5]的值,根据确定递推公式时的分析,我们想要的显然是第i-1层的dp[5],但是由于我们时从小到大的正序遍历,此时的dp[5]已经被更新,变成了第i层的dp[5]显然这样是不符合我们的要求。反之,如果倒叙遍历,就不会发生这样的情况,因为我们从大到小遍历,需要的j-weight[i]永远小于j,所以不存在需要的数据提前被覆盖的情况。
5.代码实现
public int bagTest(int[] weight,int[] value,int bagWeight){
int n=weight.length;
int[] dp=new int[bagWeight+1];
for(int i=0;i<n;i++){
for(int j=bagWeight;j>=weight[i];j++){
dp[j]=Math.max(dp[j],dp[j-weight[i]]+value[i]);
}
}
return dp[bagWeight];
}
例题练习
1.分割等和⼦集
给你一个只包含正整数的非空数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
提示:
1 <= nums.length <= 200
1 <= nums[i] <= 100
其实LeetCode上面很少有直接考察01背包问题的,大多数都是需要我们自己将问题转化为01背包问题,将陌生的题转化为我们熟悉的问题,可能大家一开始看到这道题,不会想到可以用01背包问题的解题思路来完成。我一开始做题时,看每道题都是蒙圈的。这道题居然可以用递归?这道题居然可以用动态规?而且居然可以用这么几行代码写完,再看看我自己的代码,又臭又长,一个非常简单的问题,被我想的非常复杂,看到一个题就懵逼一次。其实这些都很正常,除了少数很有天赋的人,大多数伙伴一开始接触算法时应该都会有这样的一个过程,大家只要坚持下去,多刷题,多总结,一定会有进步。
问题分析
这道题如何转化为01背包问题呢?首先我们要明确,数组中的每个元素只能被使用一次,这样可以确定是01背包问题,而不是完全背包或者其他类背包问题。
那如何判断是否可以将这个数组分割成两个等和子集?假设数组中所有元素的和为sum,如果(sum&1)==1即sum为奇数,显然不可能将这个数组分割成两个等和子集,如果sum为偶数,那么我们就可以继续分析。假设背包的容量为sum/2,每个数组中的元素当作一个物品,每个物品的重量为相应数组元素的值,每个物品的价值也是相应数组元素的值。
1.确定dp(dp table)数组及其下标的含义
dp[i][j]:在下标为[0,i]的物品任意选取,放进容量为j的背包中,所的物品的最大价值为dp[i][j]。
如果最后我们发现dp[i][j]==sum/2,其实就可以结论可以将这个数组分割成两个等和子集。那为什么可以这样判断呢?因为如果最终的dp[i][j]即dp[nums.length-1][sum/2]==sum/2,就意味着我们找到了和为sum/2的子集,另一个子集自然等于sum-sum/2=sum/2。
2.确定递推公式
经过分析转化,我们将本题转化背包容量为sum/2,物品重量为nums[i],物品价值为nums[i]的01背包问题,显然递推公式如下:
if(j>=nums[i]){
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-nums[i]]+nums[i]);
}else{
dp[i][j]=dp[i-1][j];
}
3.dp数组初始化
由递推公式可以得出我们只需要知道dp[0][j]的值即可,其他数据就可以依次递推得出。
for(int j=nums[0];j<=sum/2;j++){
dp[0][j]=nums[0];
}
4.确定遍历的顺序
从递推公式可以看出,dp[i][j]依赖于dp[i-1][j]和dp[i-1][j-nums[i]],我们想要得到dp[i][j]必须先得到dp[i-1][j]和dp[i-1][j-nums[i]],所以我们需要从前往后遍历,这也正是动态规划的精髓所在,通过子问题的答案逐步递推出最终问题的答案,避免了递归过程中大量重复相同计算子问题的答案。
5.代码实现
class Solution {
public boolean canPartition(int[] nums) {
int sum=0;
for(int num:nums){
sum+=num;
}
if((sum&1)==1) return false;
int target=sum>>1;
int[][] dp=new int[nums.length][target+1];
for(int j=nums[0];j<=target;j++){
dp[0][j]=nums[0];
}
for(int i=1;i<nums.length;i++){
for(int j=0;j<=target;j++){
if(j>=nums[i]){
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-nums[i]]+nums[i]);
}else{
dp[i][j]=dp[i-1][j];
}
}
}
return dp[nums.length-1][target]==target;
}
}
6.代码优化
通过一开始我们对经典背包问题的学习,我们发现大部分可以用二维dp数组解决的背包问题都可以压缩成一维dp数组来解决。所以我们主要的优化工作就是将二维dp数组优化为一维dp数组,代码如下。如果大家不太理解优化代码,可以看看文章开篇讲解的经典背包问题如何优化,那里讲解的很详细。
class Solution {
public boolean canPartition(int[] nums) {
int sum=0;
for(int num:nums){
sum+=num;
}
if((sum&1)==1) return false;
int target=sum>>1;
int[] dp=new int[target+1];
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;
}
}
2.最后一块石头的重量 II
有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。
提示:
1 <= stones.length <= 30
1 <= stones[i] <= 100
问题分析
这道题如何转化为01背包问题呢?首先我们要明确,数组中的每个元素只能被使用一次,这样可以确定是01背包问题,而不是完全背包或者其他类背包问题。
我们先抛开题目,假设有一块大石头,如果这个大石头的重量为偶数,我们可以将其分成重量相等的两块石头,两块石头碰撞后,直接等于0;如果这个大石头的重量为奇数,我们尽可能均分这块大石头(均分的每块石头重量都为整数),显然分成一块较重的石头和一块较轻的石头,这两块石头进行碰撞,剩下的重量即是我们所求。通过这个例子只是想让大家明白,如果想要使最后剩下的石头重量最小,我们需要尽可能的均分stones数组中的石头。
现在回到题目,因为stones数组中每块石头的重量并不相等,所以我们需要尽可能的将stones数组中的石头均分成两份。继续抽象到01背包问题,即设stones数组中石头的总重量为sum,而背包的容量为sum/2,stones数组中的每一块石头相当于每一个物品,其重量为stones[i],价值也为stones[i]。我们只需要尽可能的使背包中的物品价值最大,即背包中的石头重量最大限度接近sum/2,最后只需要让sum-dp[i][j-stones[i]]*2即可。
1.确定dp(dp table)数组及其下标的含义
dp[i][j]:在下标为[0,i]的物品任意选取,放进容量为j的背包中,所的物品的最大价值为dp[i][j]。
2.确定递推公式
经过分析转化,我们将本题转化背包容量为sum/2,物品重量为stones[i],物品价值为stones[i]的01背包问题,显然递推公式如下:
if(j>=stones[i]){
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-stones[i]]+stones[i]);
}else{
dp[i][j]=dp[i-1][j];
}
3.dp数组初始化
由递推公式可以得出我们只需要知道dp[0][j]的值即可,其他数据就可以依次递推得出。
for(int j=stones[0];j<=sum/2;j++){
dp[0][j]=stones[0];
}
4.确定遍历的顺序
从递推公式可以看出,dp[i][j]依赖于dp[i-1][j]和dp[i-1][j-nums[i]],我们想要得到dp[i][j]必须先得到dp[i-1][j]和dp[i-1][j-nums[i]],所以我们需要从前往后遍历,这也正是动态规划的精髓所在,通过子问题的答案逐步递推出最终问题的答案,避免了递归过程中大量重复相同计算子问题的答案。
5.代码实现
class Solution {
public int lastStoneWeightII(int[] stones) {
int sum=0;
for(int stone:stones){
sum+=stone;
}
int target=sum>>1;
int[][] dp=new int[stones.length][target+1];
for(int j=stones[0];j<=target;j++){
dp[0][j]=stones[0];
}
for(int i=1;i<stones.length;i++){
for(int j=0;j<=target;j++){
if(j>=stones[i]){
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-stones[i]]+stones[i]);
}else{
dp[i][j]=dp[i-1][j];
}
}
}
return sum-dp[stones.length-1][target]*2;
}
}
6.代码优化
class Solution {
public int lastStoneWeightII(int[] stones) {
int sum=0;
for(int stone:stones){
sum+=stone;
}
int target=sum>>1;
int[] dp=new int[target+1];
for(int i=0;i<stones.length;i++){
for(int j=target;j>=stones[i];j--){
dp[j]=Math.max(dp[j],dp[j-stones[i]]+stones[i]);
}
}
return sum-(dp[target]<<1);
}
}
3.⽬标和
给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
提示:
1 <= nums.length <= 20
0 <= nums[i] <= 1000
0 <= sum(nums[i]) <= 1000
-1000 <= target <= 1000
问题分析
这道题如何转化为01背包问题呢?首先我们要明确,数组中的每个元素只能被使用一次,这样可以确定是01背包问题,而不是完全背包或者其他类背包问题。
我们假设nums数组中的所有元素和为sum,整数前添加‘+’的整数和为add,显然添加‘-’的整数和为sum-add,所以target=add-(sum-add);化简转化可以得到add=(sum+target)/2,现在我们只需要在nums数组中找到所有添加‘+’的整数并且满足add=(sum+target)/2,由题意可知nums数组中的元素都是整数,如果(sum+target)为奇数,那么add为小数,显然不和题意,这种情况直接返回0,即找不到满足题意的表达式。如果为偶数,我们就可以继续分析。现在这道题变成了在数组中寻找需要添加‘+’的数字,直至数字和满足add=(sum+target)/2,有没有感觉很熟悉,这像不像在背包容量为(sum+target)/2,中寻找物品,直至尽可能装满背包,是背包中的物品价值最大。
现在我们可以假设背包容量为(sum+target)/2,nums数组中的每一个元素相当于一个物品,每一个元素值相当于物品的重量。
1.确定dp(dp table)数组及其下标的含义
dp[i][j]:在下标为[0,i]的物品任意选取,放进容量为j的背包中,装满背包的次数为dp[i][j]。
注意:之前都是求背包中物品的最大价值,本题变成了装满背包的次数,这也对应着题目求满足条件的表达式的数组。
2.确定递推公式
这道题的递推公式和之前的也有一点不一样。大家要时刻注意dp[i][j]代表的含义,因为后续工作都是由dp数组的含义展开的。我们来回忆一下dp数组的含义。
dp[i][j]:在下标为[0,i]的物品任意选取,放进容量为j的背包中,装满背包的次数为dp[i][j]。
当我们遍历到第i个元素时,如果j<nums[i],显然此时我们不能将第i个物品放入背包,所以dp[i][j]=dp[i-1][j]。如果j>=nums[i],那么我们有两种选择,一种是不将第i个元素放入背包,那么dp[i][j]=dp[i-1][j],另一种是将第i个元素放入背包,那么dp[i][j]=dp[i-1][j-nums[i]]。因为本题是要求满足条件的表达式数组,所以dp[i][j]=dp[i-1][j]+dp[i-1][j-nums[i]]。如果还是不太理解可以去看看文章开篇分析经典背包问题的讲解,哪里讲解的比较详细。
if(j>=nums[i]){
dp[i][j]=dp[i-1][j],dp[i-1][j-nums[i]];
}else{
dp[i][j]=dp[i-1][j];
}
3.dp数组初始化
由递推公式可以得出我们只需要知道dp[0][j]的值即可,需要注意的是dp[0][0],若nums[0]=0,那么dp[0][0]=2,因为我们有两种选择,一种是将nums[0]放入背包,另一种是不放入背包。其他需要初始化的数据dp[0]j,如果nums[0]==j那么dp[0][j]=1,这是因为如果nums[0]==j,说明只要将第0个物品放入背包即可装满背包,否则就没有装满背包的方法。
dp[0][0]=1;
for(int j=0;j<=(sum+target)/2;j++){
if(j==nums[0]){
dp[0][j]+=1;
}
}
4.确定遍历的顺序
从递推公式可以看出,dp[i][j]依赖于dp[i-1][j]和dp[i-1][j-nums[i]],我们想要得到dp[i][j]必须先得到dp[i-1][j]和dp[i-1][j-nums[i]],所以我们需要从前往后遍历,这也正是动态规划的精髓所在,通过子问题的答案逐步递推出最终问题的答案,避免了递归过程中大量重复相同计算子问题的答案。
5.代码实现
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int sum=0;
for(int num:nums){
sum+=num;
}
//如果为奇数说明没有满足条件的表达式,直接返回0
if(((sum+target)&1)==1) return 0;
//如果sum<=math.abs(target) 那么就算所有的整数都添加‘+’或者‘-’都无法满足条件
if(sum<Math.abs(target)) return 0;
int add=(sum+target)>>1;
int[][] dp=new int[nums.length][add+1];
//dp数组初始化
dp[0][0]=1;
for(int j=0;j<=add;j++){
if(j==nums[0]){
dp[0][j]+=1;
}
}
for(int i=1;i<nums.length;i++){ //遍历物品
for(int j=0;j<=add;j++){ //遍历背包容量
if(j>=nums[i]){
dp[i][j]=dp[i-1][j]+dp[i-1][j-nums[i]];
}else{
dp[i][j]=dp[i-1][j];
}
}
}
return dp[nums.length-1][add];
}
}
6.代码优化
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int sum=0;
for(int num:nums){
sum+=num;
}
//如果为奇数说明没有满足条件的表达式,直接返回0
if(((sum+target)&1)==1) return 0;
//如果sum<=math.abs(target) 那么就算所有的整数都添加‘+’或者‘-’都无法满足条件
if(sum<Math.abs(target)) return 0;
int add=(sum+target)>>1;
int[] dp=new int[add+1];
dp[0]=1;
for(int i=0;i<nums.length;i++){
for(int j=add;j>=nums[i];j--){
dp[j]+=dp[j-nums[i]];
}
}
return dp[add];
}
}
4.⼀和零
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
提示:
1 <= strs.length <= 600
1 <= strs[i].length <= 100
strs[i] 仅由 '0' 和 '1' 组成
1 <= m, n <= 100
问题分析
这道题如何转化为01背包问题呢?首先我们要明确,数组中的每个元素只能被使用一次,这样可以确定是01背包问题,而不是完全背包或者其他类背包问题。
这道题大家一开始看,可能不会想到转化为01背包问题来解决。其实这也恰恰是背包问题的难点所在。因为很少会直接考察背包问题,都需要我们将问题转化为背包问题来解决。
之前我们考虑一个物品能否放入背包都是比较背包容量是否大于当前物品重量,如果大于就可以放进去,反之则不可以。这道题其实也是这样,只不过有增加了一个条件。我们假设一个背包能够装i个0,j个1,数组中的每个字符串元素相当于一个物品,这个物品包含x个0,y个1,如果i>=x&&j>=y,说明物品可以放进背包,反之则不可以放入背包。物品价值其实都是1,因为没放入一个物品,背包中的物品数量会加1。
1.确定dp(dp table)数组及其下标的含义
dp[k][i][j]:在下标为[0,k]的物品任意选取,放进数字0容量为i,数字1容量为j的背包中,可以放进背包的物品数量最多为dp[k][i][j]。
翻译过来其实就是:找到strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。m和n对应背包的两个容量限制i和j。最大子集长度就是放入背包的最大物品数量。
2.确定递推公式
这道题的递推公式和之前的也有一点不一样。大家要时刻注意dp[k][i][j]代表的含义,因为后续工作都是由dp数组的含义展开的。我们来回忆一下dp数组的含义。
dp[k][i][j]:在下标为[0,k]的物品任意选取,放进数字0容量为i,数字1容量为j的背包中,可以放进背包的物品数量最多为dp[k][i][j]。
当我们遍历到第k个字符串元素时,假设第k个字符串含有x个0和y个1,这是会有两种情况。
第一种:i<x||j<y,这种情况就比较简单了,因为背包装不下物品k,显然dp[k][i][j]=dp[k-1][i][j]。
第二种:i>=x&&j>=y,这时候我们有两种选择,一种是不把物品k放入背包,我们在文章开篇分析经典01背包问题是就详细分析过,我们使用背包的总容量i和j去和字符串元素的x和y去做比较,在这之前我们可能已经往背包种放入许多物品,此时背包的对数字0和1的容量可能已经远小于i和j,所以如果选择将k放入背包,势必要清除一部分物品,可能是清除0个,1个,甚至多个,如果装进背包的第k个物品的价值不能抵扣因为要腾出空间来装物品k而清除的物品的价值总和,那么放进物品k就有些得不偿失了,所以我们才会有不放进物品k,保持原状的这一种选择,而不是如果物品k没有超过背包最大容量限制就直接把他放入背包。此时dp[k][i][j]=dp[k-1][i][j];当然还有另一种选择就是将第k件物品放入背包,那就势必要清除一部分物品为物品k让路,同时将第k件物品放入背包,背包的总价值也会变为,清除一部分物品后背包中物品剩余的价值和第k件物品的价值综合。即dp[k][i][j]=dp[k-1][i-x][j-y]+1。
当然由于dp[k][i][j]表示的是在下标为[0,k]的物品任意选取,放进数字0容量为i,数字1容量为j的背包中,可以放进背包的物品数量最多为dp[k][i][j],所以每个物品的价值都可以视作1,因为每个物品都只能为背包中物品数量贡献1。
综上,递推公式如下
if(i>=x&&j>=y){
dp[k][i][j]=Math.max(dp[k-1][i][j],dp[k-1][i-x][j-y]+1);
}else{
dp[k][i][j]=dp[k-1][i][j];
}
3.dp数组初始化
由递推公式可以得出我们只需要知道dp[0][i][j]的值即可,其他数据都可以通过递推得出。设第0个字符串元素含有x个0和y个1。
for(int i=0;i<=m;i++){
for(int j=0;j<=n;j++){
dp[0][i][j]==(i>=x&&j>=y)?1:0;
}
}
4.确定遍历的顺序
从递推公式可以看出,dp[k][i][j]依赖于dp[k-1][i][j]和dp[k][i-x][j-y]],我们想要得到dp[k][i][j]必须先得到dp[k-1][i][j]和dp[k][i-x][j-y]],所以我们需要从前往后遍历,这也正是动态规划的精髓所在,通过子问题的答案逐步递推出最终问题的答案,避免了递归过程中大量重复相同计算子问题的答案。
5.代码实现
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
int len=strs.length;
//存放每个字符串元素含有的0和1的个数
int[][] num=new int[len][2];
//遍历每一个字符串元素,获取其0和1的个数
for(int i=0;i<len;i++){
char[] chars=strs[i].toCharArray();
for(char c:chars){
if(c=='0'){
num[i][0]+=1;
}else{
num[i][1]+=1;
}
}
}
int[][][] dp=new int[len][m+1][n+1];
//dp数组初始化
for(int i=0;i<=m;i++){
for(int j=0;j<=n;j++){
dp[0][i][j]=(i>=num[0][0]&&j>=num[0][1])?1:0;
}
}
for(int k=1;k<len;k++){
for(int i=0;i<=m;i++){
for(int j=0;j<=n;j++){
if(i>=num[k][0]&&j>=num[k][1]){
dp[k][i][j]=Math.max(dp[k-1][i][j],dp[k-1][i-num[k][0]][j-num[k][1]]+1);
}else{
dp[k][i][j]=dp[k-1][i][j];
}
}
}
}
return dp[len-1][m][n];
}
}
6.代码优化
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
int[][] dp=new int[m+1][n+1];
for(String str:strs){
int zeroNum=0,oneNum=0;
char[] chars=str.toCharArray();
for(char c:chars){
if(c=='0'){
zeroNum+=1;
}else{
oneNum+=1;
}
}
for(int i=m;i>=zeroNum;i--){
for(int j=n;j>=oneNum;j--){
dp[i][j]=Math.max(dp[i][j],dp[i-zeroNum][j-oneNum]+1);
}
}
}
return dp[m][n];
}
}