0-1背包问题
背包问题是经典的dp问题,可以分为0-1背包和完全背包,这里我们讨论0-1背包问题;
问题场景一般是:
背包最大容量为V,物品数量为N;
体积数组:v[0…N-1] = {…}
价值数组:c[0…N-1] = {…}
求解能装下的最大价值,每个物品要么拿要么不能,只能拿一次;
我们定义dp二维数组:dp[i][j]表示面对前i个物品,背包容积为j时,能获得的最大价值
那么状态转移方程:
dp[i][j] = max(dp[i-1][j],dp[i-1][j-v[i]]+c[i])
那么主要代码可以表示为:
for(int i=1;i<v.size();i++)
{
for(int j=0;j<=V;j++)
{
dp[i][j] = dp[i-1][j];
if(j-v[i]>=0)
dp[i][j] = max(dp[i-1][j],dp[i-1][j-v[i]+c[i]);
}
}
还可以进一步简化为一维数组,但是要注意了正推和逆推会导致完全背包和0-1背包!
我们来看正推:
for(int i=1;i<v.size();i++)
{
for(int j=v[i];j<=V;j++)
{
if(j-v[i]>=0)
dp[j] = max(dp[j],dp[j-v[i]+c[i]);
}
}
注意这是完全背包!这会有可能再次去哪之前拿过了的物品!因为是由前往后推的,前面对某一物品拿取的结果会覆盖掉之前没有拿的结果,导致推到后面的时候用了已经拿过的结果作为记忆而导致重复拿取;
0-1背包拿了之后不能再拿,因此只能用逆推!
for(int i=1;i<v.size();i++)
{
for(int j=V;j>=v[i];j++)
{
if(j-v[i]>=0)
dp[j] = max(dp[j],dp[j-v[i]]+c[i]);
}
}
分割等和子集
子集和的问题其实是背包问题的一种变体,这种问题一般本质上是问一个数列是否能有一个子集使得子集的和等于某个期望的数!而一个数到底放不放入子集取决于不放入是否能达到期望和或放入的时候能否达到期望和!
可以先求数组的和,若为奇数那么肯定为false;
然后可以以sum/2为目标,每个数可以分为拿和不拿,就像01背包一样,定义dp数组:
dp[i][j]表示,对于前i个数,子集和为j的可行性,可行则为1,不可行为0
那么得到状态转移方程:
dp[i][j] = dp[i-1][j] | dp[i-1][j-nums[i]]
代码:
class Solution {
public:
bool canPartition(vector<int>& nums) {
if(nums.size()==1)
return false;
int sum = 0;
for(int i=0;i<nums.size();i++)
sum += nums[i];
if(sum%2 == 1)
return false;
sum = sum/2;
cout<<sum;
vector<vector<int>> dp(nums.size(),vector<int>(sum+1,0));
dp[0][0] = dp[0][nums[0]] = 1;
for(int i=1;i<nums.size();i++)
{
for(int j=0;j<=sum;j++)
{
dp[i][j] = dp[i-1][j];
if(j-nums[i]>=0)
dp[i][j] = dp[i-1][j]|dp[i-1][j-nums[i]];
}
}
if(dp[nums.size()-1][sum] == 0)
return false;
else
return true;
}
};
简化为一维dp:
class Solution {
public:
bool canPartition(vector<int>& nums) {
if(nums.size()==1)
return false;
int sum = 0;
for(int i=0;i<nums.size();i++)
sum += nums[i];
if(sum%2 == 1)
return false;
sum = sum/2;
cout<<sum;
vector<int> dp(sum+1,0);
dp[0] = dp[nums[0]] = 1;
for(int i=1;i<nums.size();i++)
{
for(int j=sum;j>=0;j--)
{
if(j-nums[i]>=0)
dp[j] = dp[j]|dp[j-nums[i]];
}
}
if(dp[sum]==0)
return false;
else
return true;
}
};