0-1背包问题
假设N件物品,每件物品重量为w[i],每件物品的价值为v[i],背包重量为C,选择物品装入背包使其在不超过负重的前提下价值最大,每种物品只能用一次。
假设dp[i][j]表示在前i件物品,背包重量为j的前提下的最大价值
则如果物品可以放入背包,则动态转移方程为 dp[i][j]=max( dp[i-1][j],dp[i-1][j-w[i]]+v[i] ) , j>=w[i]
如果物品不能放入背包,dp[i][j]=dp[i-1][j] ,j<w[i]
#include <bits/stdc++.h>
using namespace std;
#define M 1010
int dp[M][M]; //数组太大定义为全局变量,一方面自动初始化,一方面数组范围可以更大
int main()
{
int m,N,C,w[M],v[M];
cin>>m;
while(m--){
cin>>N>>C;
for(int i=1;i<=N;i++) cin>>v[i];
for(int i=1;i<=N;i++) cin>>w[i]; //这里数组是w和v都是从下标1开始存储数据的
for(int i=1;i<=N;i++){
//一个二维表,从第二行开始计算,第一行全部初始化为0
for(int j=0;j<=C;j++)
if(j>=w[i]) dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
else dp[i][j]=dp[i-1][j]; //dp数组默认初始化,默认全是0,所以第一行dp[0][j]也全部是0,
}//就不用再额外自己初始化了,即for(int j=0;j<=C;j++) dp[0][j]=0;省略
cout<<dp[N][C]<<endl;
}
return 0;
}
为什么说要把打表出来的数组dp第一行全部初始化为0呢?
因为dp[0][j] 是指没有任何物品的前提下装满容量为j的背包,价值肯定为0,则dp[1][j]代表第一件物品装满容量j的背包的最大价值,注意,看数组w和v中存储数的下标,有些是从0开始存储数据的,此时对应w[i-1],有些是从下标1存储数据的,则直接对应下标w[i],下面的例子都是这样的道理,不再赘述!
下面对空间复杂度进行优化,观察到dp[i][j]的取值取决于上一行dp[i-1][j],所以在只要最终结果的前提下,我们只需要保留两行就行了,第0行和第1行,利用滚动数组,使其不断在0和1之间变换:
利用异或,假设p=0; 让p一直和1异或 ,p=p^1=1; p=p^1=0; p=p^1=1,就这样一直变换
#include <bits/stdc++.h>
using namespace std;
#define M 1010
int dp[2][M];
int main()
{
int m,N,C,w[M],v[M];
cin>>m;
while(m--){
cin>>N>>C;
int p=0;
memset(dp[p],0,sizeof(dp[p]));//由于是多次输入,得对第0行进行初始化,不能再依靠系统的默认初始化
for(int i=1;i<=N;i++) cin>>v[i];
for(int i=1;i<=N;i++) cin>>w[i];
for(int i=1;i<=N;i++){
p^=1; //在第0行和第1行之间来回切换
for(int j=0;j<=C;j++)
if(j>=w[i]) dp[p][j]=max(dp[p^1][j],dp[p^1][j-w[i]]+v[i]);
else dp[p][j]=dp[p^1][j];
}
cout<<dp[p][C]<<endl;
}
return 0;
}
上面的滚动数组数组属于过渡阶段,可以借鉴这样的思想,采用这样的思路直接使用一维数组。
注意到,算dp[i][j]时只于它上一行在第j列之前的数组有关,所以我们在第二层for循环时采用倒序,从j=C入手,逐渐递减,判定结束的条件是背包容量j是否大于当前要装入背包的物品的重量w[i],因为当小于w[i]时,由前面知是直接dp[i][j]=dp[i-1][j],继承上一行的数据,由于我们采用一维数组,可直接继承。
#include <bits/stdc++.h>
using namespace std;
#define M 1010
int dp[M];
int main()
{
int m,N,C,w[M],v[M];
cin>>m;
while(m--){
memset(dp,0,sizeof(dp));
cin>>N>>C;
for(int i=1;i<=N;i++) cin>>v[i];
for(int i=1;i<=N;i++) cin>>w[i];
for(int i=1;i<=N;i++){
for(int j=C;j>=w[i];j--) //倒序
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}//注意第二层for循环结束的条件是j>=c[i],因为当j<c[i]时,不做处理,默认继承上一行的数据
cout<<dp[C]<<endl;
}
return 0;
}
LeetCode416. 分割等和子集
这题相当于是背包负重sum/2,物品的重量和价值都是nums[i],价值V给出,希望是sum/2。
分析:
二维数组dp[i][j]表示前i件物品能否使容量为j的背包价值为j,能则为1/true,不能则为0/false。
动态转移方程为dp[i][j]=dp[i-1][j] || dp[i-1][j-nums[i-1]] //i-1是因于代码给出的nums数组是从0开始存储的
方程可以理解为:如果前i-1件物品已经凑够价值j了,那么前i件物品也肯定可以,或者前i-1件物品凑出了j-nums[i-1]的价值,再加上这个物品,就够j了,也为true。
下面简化成一维数组dp
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum=0,size=nums.size();
for(int i=0;i<size;i++) sum+=nums[i];
if(sum%2) return false; //如果是奇数,肯定不能分割
sum/=2;
int dp[sum+1];
memset(dp,0,sizeof(dp));
dp[0]=1; //和前面的一般01背包进行理解,思考为什么这里要为1?
for(int i=1;i<size+1;i++)
for(int j=sum;j>=nums[i-1];j--)
dp[j]=dp[j]||dp[j-nums[i-1]];
return dp[sum];
}
};
dp[0][j] 表示前0件物品,即空,使背包价值为j,显而易见,当j为0时,dp[0][0]为1。
所以在用一维数组处理时,dp[0]为1,其它初始化为0。
LeetCode1049. 最后一块石头的重量 II
此题换个理解方式即为:将这组石头分成两组,使这相差的重量之差最小,返回这个差值。
求出一个分组的重量就行,假设总重量为ss的石头分为重量为a和重量为b的两组(b>=a),那么差值即为b-a,等价于(b+a)-(a+a),即ss-2*a,且两组分组肯定有一组大于等于ss/2,一组小于等于ss/2。
因此,将问题转换为0-1背包问题,对于重量和价值均为stones[i]的石头,背包负重为ss/2,求最大价值。标准0-1背包模板!
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
int size=stones.size();
int ss=0;
for(int i=0;i<size;i++) ss+=stones[i];
int sum=ss/2;
int dp[sum+1];
memset(dp,0,sizeof(dp));
for(int i=1;i<size+1;i++)
for(int j=sum;j>=stones[i-1];j--)
dp[j]=max(dp[j],dp[j-stones[i-1]]+stones[i-1]);
return ss-2*dp[sum];
}
};
LeetCode494. 目标和
添加正负号,通过移项变号可知,此题和上面的题一样,也是把数组分成两组,假设数组和为sum,分成的两组中A组和为a,B组和为b,如果成立的话,则有a+b=sum,a-b=S,那么可知a=(sum+S)/2,b=(sum-S)/2。
即如果sum<S或者sum+S/sum-S不是2的倍数的话,就不能划分成功,即有0种方法。
动态转移方程:dp[i][j]=dp[i-1][j]+dp[i-1][j-nums[i-1]],即考虑加入nums[i-1]和不加入nums[i-1]两种情况下的方法总数,如果j<nums[i-1],即不能加入,直接取dp[i-1][j]的值。
同样的,dp[0]][0]表示0个数分组相差为0的情况,所以dp[0]][0]=1,其它dp[0][j]=0。
同理采用一维数组表示:
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int S) {
int n=nums.size();
long long sum=0; //防止sum超出范围
for(int i=0;i<n;i++) sum+=nums[i];
if((sum+S)%2==1 || sum<S) return 0;
sum=(sum+S)/2; //算其中一个就行
int dp[sum+1];
memset(dp,0,sizeof(dp));
dp[0]=1;
for(int i=1;i<n+1;i++)
for(int j=sum;j>=nums[i-1];j--)
dp[j]+=dp[j-nums[i-1]];
return dp[sum];
}
};
完全背包问题
与0-1背包不同的一点在于对于每个物品,不是取(1)或者不取(0)两种状态,而是可以取0,1,2…任意次,其动态转移方程满足:
dp[i] [j]=max(dp[i-1] [j-k×w[i]]+k×v[i]),其中 0 ≤ k×w[i] ≤ j
可以发现,当k只能取0,1时的特例就是简单的0-1背包问题,这个可以进一步化简,考虑当该物品不加入背包时,dp[i][j]取决于dp[i-1][j],而当选择加入时,该物品可能已经加入了,dp[i][j-w[i]]≥dp[i-1][j-w[i]]必然成立,所以动态转移方程:
dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+v[i])
这样的话,再看其一维数组的化简,不再像简单的0-1背包那样第二层循环进行倒序了,这次需要正序,因为需要得到dp[i][j-w[i]]+v[i]),之前倒序是因为需要保留dp[i-1][j-w[i]]+v[i])。
#include <bits/stdc++.h>
using namespace std;
//完全背包,可以重复选取物品。
int main()
{
int m,n,w[35],v[35];
cin>>m>>n; //背包上限m,n个物品
for(int i=0;i<n;i++) cin>>w[i]>>v[i];
int dp[m+1];
memset(dp,0,sizeof(dp));
for(int i=1;i<n+1;i++)
for(int j=w[i-1];j<m+1;j++)
dp[j]=max(dp[j],dp[j-w[i-1]]+v[i-1]); //这条语句和简单0-1背包一样
cout<<"max="<<dp[m];
//通过与一维数组形式的简单0-1背包对比可以发现只有第二层for循环不一样,其它都一样
return 0;
}
LeetCode322. 零钱兑换
这题是反向,背包容量固定,求装满背包的最小物品个数,物品可以重复。
每个硬币的面额coins[i],即物品价值v[i],面额各不相同的硬币有n个,背包容量amount,即C。
假设dp[i][j]表示使用前i种硬币装满容量为j的背包所需的最少硬币数,动态转移方程为:dp[i][j]=min(dp[i-1][j],dp[i][j-coins[i-1]]+1)
此外,我们取的是最小个数,所以对二维数组dp初始化时,第一行dp[0][j]的初始化应尽可能无穷大,表示无法取得,但是dp[0][0]是可以取得的,且dp[0][0]=0。
一维数组进行空间优化: 采用dp[count+1],dp[0]=0。别忘了第二层循环背包容量从小到大采用正序。
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int MAX=0x3f3f3f;
int n=coins.size();
vector<int> dp(amount+1,MAX);
dp[0]=0;
for(int i=1;i<n+1;i++)
for(int j=coins[i-1];j<amount+1;j++) //正序
dp[j]=min(dp[j],dp[j-coins[i-1]]+1);
if(dp[amount]!=MAX) return dp[amount];
else return -1;
}
};