日常力扣刷题,今天做到这样一道题:
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。(力扣416题,难度中等)
我的解题思路:
①.看到数组以及是需要给出最后结果,我一般都会先考虑动态规划,而动态规划的难点就在于如何写出状态转移方程,在本题目中很难看出数组之间存在什么联系。于是就先放弃这个想法,显而易见的是如果能分成两个数组,那么俩数组之和一定相等并且为nums数组之和的一半,所以我就先求出nums之和sum,当然,sum肯定得是偶数。
②.那么现在问题就变成了在nums数组里面找出n个数,加起来正好为sum/2(每个数都能选一次)。
③.一开始的思路肯定是用DFS,一个数一个数地累加,但是发现超时了(虽然本来就有猜想会超时,但还是会去写一下),然后想到有重复状态的出现,于是改成记忆化搜索,既然是记忆化,就可以用一个数组来记录状态,这样就又有点像动态规划了。
④.仔细一想,从数组里取出n个数字,每个数字可能被取走(1),也可能没被取走(0),这就有点像[0-1]背包问题了。
什么是[0-1]背包问题?如何求解呢?
[0-1]背包问题是[背包问题]的其中一种,那么什么是[0-1]背包问题呢?不妨看看下面这道题([0-1]背包问题经典题目):
已知有n个物品,每个物品的重量记录在weight[n],一个数组price[n]代表每个物品的价格,有一个背包能够承受的重量为target,求如何选取物品才能在不超重的情况下拿到的物品价格总和最大?
注:每个物品只能拿一次!
解题:
①.很明显,这种最值问题大多都是用动态规划来求解的,那么这个问题的关键就在于如何找出状态转移方程呢?
②.那么如何找出状态转移方程呢?一般情况下我们都是创建一个动态数组dp[n],但是在这题上面我们能难找到它们之间的联系,因为每个物品都有两个值,一个是重量,一个是价格。那么我们能不能创建一个二位动态数组来进行转换呢?想到这里,我们不妨先创建一个二维数组。
③.创建二维数组要创建多大呢?因为我们最后只需要一个结果,所以有一个值肯定是所给的target,那另一个选什么呢?在一般的一维动态规划中我们不都是选取所给数组的大小为创建大小吗?所以我们不如也选取给的重量的数组的大小为一个值?(为什么选重量呢?因为所给的target是与重量有关的,当然了,这里选那个都一样,都是n)。所以我们创建一个二维的动态数组dp
vector<vector<int>> dp(n+1,vector<int> (target+1,0));
//dp[i][j]表示选取[0-i]个物品中,重量加起来不超过j时的最大值
④.首先我们先定义初始条件:当选取0个物品时,无论背包所能承受的重量多大都没关系,所以dp[0][j]=-1;当背包承受的重量为0的时候,无论有多少个物品可以选择,能选择的数量都只能是0,所以dp[i][0]=0;
⑤.开始找状态转移方程,dp[i][j]表示能选的物品加入weight[i-1]的时候,不超过背包承重为j时的最大价格,那么此时,就有俩个选择
在这个背包里面添加这个物品,但想要添加成功就不能超重,那么就需要j>weight[i-1];此时的dp[i][j]=dp[i][j-weight[i-1]]+price[i];
不在背包里添加这个物品,那么dp[i][j]=dp[i-1][j];
⑥.综上,dp[i][j]=max(dp[i][j-weight[i-1]]+price[i],dp[i-1][j]);
⑦.dp[n][target]就是最后的结果
代码模板如下:
class Solution
{
public:
int MaxPrice(vector<int>& weight,vector<int>& price,int target)
{
int n=weight.size();
//创建二维数组
vector<vector<int>> dp(n+1,vector<int> (target+1,0));
//初始化数组
for(int i=1;i<dp[0].size();i++)
{
dp[0][i]=-1
}
//进行状态转移
for(int i=1;i<dp.size();i++)
{
for(int j=1;j<dp[0].size();j++)
{
if(j>=weight[i-1])
{
dp[i][j]=max(dp[i][j-weight[i-1]]+price[i],dp[i-1][j]);
}
}
}
return dp[n][target];
}
};
建议画个图可以更深刻的理解逻辑,并且会发现,每一行的数据都只和上一行的数据或者上一行的左边某几列有关系,既然这样,我们完全可以优化一下我们的代码,将二维数组变成一维数组
vector<int> dp(target+1);
dp[weight[0]]=price[0];
for(int i=1;i<n;i++)
{
for(int j=target;j>weight[i];j--)
{
dp[j]=max(dp[j],dp[j-weight[i]]+price[i]);
}
}
return dp[target]
有小伙伴会问,为什么内循环是逆序啊?这边建议自己按照流程画个图就知道为什么了捏!
[背包问题]不只有[0-1]背包问题,还有[完全背包]等等,想要完全搞懂的话建议去看下面这篇文章,里面讲解的很详细:http://t.csdn.cn/e782s
回到原本的题目
既然学会了[0-1]背包问题,那么这题也就会做了吧,唯一的区别就是这题需要加起来的值=sum/2,但这并不妨碍用[0-1]背包问题的模板来解题,代码如下:
class Solution {
public:
bool canPartition(vector<int>& nums)
{
sort(nums.begin(),nums.end());
int sum=0;
for(int i=0;i<nums.size();i++)
{
sum+=nums[i];
}
if(sum%2!=0)
{
return false;
}
else
{
int mid=sum/2;
if(nums[nums.size()-1]>mid)
{
return false;
}
else
{
int j=nums.size()-1;
vector<vector<bool>> dp(nums.size()+1,vector<bool> (mid+1,false));
for(int i=0;i<dp.size();i++)
{
dp[i][0]=true;
}
for(int i=1;i<dp.size();i++)
{
for(int j=1;j<dp[0].size();j++)
{
if(j>=nums[i-1])
{
dp[i][j]=dp[i-1][j]|dp[i-1][j-nums[i-1]];
}
else
{
dp[i][j]=dp[i-1][j];
}
}
if(dp[i][mid])
{
return true;
}
}
return dp[nums.size()][mid];
}
}
}
};
空间优化后的代码:
class Solution {
public:
bool canPartition(vector<int>& nums)
{
sort(nums.begin(),nums.end());
int sum=0;
for(int i=0;i<nums.size();i++)
{
sum+=nums[i];
}
if(sum%2!=0)
{
return false;
}
else
{
int mid=sum/2;
if(nums[nums.size()-1]>mid)
{
return false;
}
else
{
int j=nums.size()-1;
vector<bool> dp(mid+1,false);
dp[0]=true;
dp[nums[0]]=true;
for(int i=1;i<nums.size();i++)
{
for(int j=mid;j>=nums[i];j--)
{
if(dp[mid])
{
return true;
}
dp[j]=dp[j]|dp[j-nums[i]];
}
}
return dp[mid];
}
}
}
};
今天的学习就到这边了,想要掌握好背包问题的话,建议去把力扣上的有关问题都做一遍
「力扣」上的 0-1 背包问题:
「力扣」第 416 题:分割等和子集(中等);
「力扣」第 474 题:一和零(中等);
「力扣」第 494 题:目标和(中等);
「力扣」第 879 题:盈利计划(困难);
「力扣」上的 完全背包问题:
「力扣」第 322 题:零钱兑换(中等);
「力扣」第 518 题:零钱兑换 II(中等);
「力扣」第 1449 题:数位成本和为目标值的最大数字(困难)。
这里要注意鉴别:「力扣」第 377 题,不是「完全背包」问题。
时间:2023.1.13