背包问题分为01背包与完全背包
01背包,共有N个物体,每个物体只有一个,装入给定背包中
完全背包,共有N种物体,每个物体数量不限,装入给定背包中
01背包
重点:
1.思路:对每个物体i,在剩余容量j时选择装与不装
2.注意一维情况时的容量倒序遍历
问题定义:给定容量V的背包,和体积分别为{Ci}(i=1...N)的N个物体,每个物体价值为{Wi}(i=1..N).求使得背包价值最大的装法。
状态:对于每个物体i,在背包容量为j时,背包价值为DP(i,j)
选择:
如果当前背包体积j>=Ci,有装与不装当前物体两种选择
选择装入当前物体,DP(i,j)=DP(i-1,j-Ci)+Wi (j>=Ci)
不装入当前物体, DP(i,j)=DP(i-1,j)
如果剩余背包体积
故转移方程:
for i=1....N
for j=0...V
if(j>=Ci) DP(i,j)=max{DP(i-1,j),DP(i-1,j-Ci)+Wi};
if(j
优化为1维数组
由原始递推式,DP(i,j)仅仅依赖于DP(i-1,XX)状态,考虑优化为1维数组。由于计算DP(i,j)时需要保证DP(i-1,j-Ci)还没有更新,因此容量需要倒序遍历。
for i=1....N
for j=V...0 //此处注意:由于在计算DP(j)时需要DP(j-Ci)依然保持在i-1的状态,所以V需要倒序遍历
if(j>=Ci)DP(j)=max{DP(j),DP(j-Ci)}
01背包其他问法:根据问法更改转移方程的函数
<1>是否能恰好装满
二维状态定义:DP(i,j)代表第i个物体容量为j时是否恰好装满
二维状态转移方程:
DP(i,j)=DP(i-1,j)||DP(i-1,j-Ci) (j>=Ci)
DP(i,j)=DP(i-1,j) (j=Ci)(j=V....0) //注意01背包问题1维状态需要倒序遍历
初始化
DP[0]=TRUE //容量为0时一个物体都不选恰好能装满,故DP[0]=1;
DP[1....V]=FALSE
<2>恰好装满的方案总数
二维状态定义:DP(i,j)代表第i个物体容量为j时恰好装满的总方案数
二维状态转移方程:
DP(i,j)=DP(i-1,j)+DP(i-1,j-Ci) (j>=Ci)
DP(i,j)=DP(i-1,j) (j=Ci)(j=V....0) //注意01背包问题1维状态需要倒序遍历
初始化
DP[0]=1 //容量为0时至少存在 一个物体都不选这个方案,故初始化为1
DP[1....V]=0
<3>恰好装满所需最少物体数
二维状态定义:DP(i,j)代表第i个物体容量为j时恰好装满所需最少物体数,物体数方案不存在返回-1
二维状态转移方程:
DP(i,j)=min{DP(i-1,j),DP(i-1,j-Ci)+1}(j>=Ci)
DP(i,j)=DP(i-1,j) (j
完全背包
问题定义:给定n种物体,每种物体数量不限,每种物体的体积为Ci,价值为Wi。
向容量为V的背包中装入这n种物体,求背包能装入的最大价值。与01背包的最大不同是,此时每种物体的数量不限。
状态定义:在对第i种物体进行选择,背包体积为j时。DP(i,j)表示此时背包最大价值
选择:
当前物体体积小于背包容积j,则可选择装入或不装入
装入:DP(i,j)=DP(i,j-Ci)+Wi //此处与01背包最大不同是,由于选择装入物体i后,可以选择继续装入物体i。故此处为DP(i,j-Ci)而不是DP(i-1,j-Ci)
不装入:DP(i,j)=DP(i-1,j)
当物体i体积大于j,只能选择不装入
DP(i,j)=DP(i-1,j)
状态转移方程:
DP(i,j)=max{DP(i-1,j),DP(i,j-Ci)+Wi} (j>=Ci)
DP(i,j)=DP(i-1,j) (j
完全背包一维优化
由状态转移方程,DP(i,j)最多依赖于DP(i-1,j),且计算DP(i,j)时需要DP(i,j-Ci)已经计算。
故一维状态方程
for i=1.....N
for j=Ci....V //为保证j>=Ci ,j从Ci开始遍历
DP(j)=max{DP(j),DP(j-Ci)+Wi} //注意此时对容量遍历是正序遍历,与01背包正好相反
其他问法:
完全背包也包括恰好装满方案数、装满所需最少数量、能否恰好装满等问法。
416 分割等和子集(0-1背包,是否恰好能装下)
bool canPartition(vector<int>& nums) {
if(nums.empty()){
return false;
}
unsigned int sum=0;
for(auto i:nums) sum+=i;
if(sum%2!=0) return false;
//问题转化为在nums中是否存在n个数之和恰好为sum/2
//转化为01背包问题,求是否能恰好装满
unsigned int target=sum/2;
vector<vector<bool>>dp(nums.size()+1,vector<bool>(target+1,false));
dp[0][0]=true; //dp[0][0]代表一个物体都不选,恰好能装满容量为0背包
//转移方程
//DP(i,j)=DP(i-1,j)||DP(i-1,j-Ci) j>=Ci
//DP(i,j)=DP(i-1,j) j
for(int i=1;i<=nums.size();++i){
for(int j=0;j<=target;++j){
//当前选择第i个物体,体积为nums[i-1],背包容量为j.
int Ci=nums[i-1];
if(j>=Ci){
dp[i][j]=dp[i-1][j]||dp[i-1][j-Ci]];
}else{
dp[i][j]=dp[i-1][j];
}
}
}
return dp[nums.size()][target];
}
优化为1维状态
bool canPartition(vector<int>& nums) {
if(nums.empty()) return false;
int sum=0;
for(auto i:nums)sum+=i;
if(sum%2==1) return false;
int pack=sum/2;//背包容量为和的一半
//01背包问题:状态方程
//F(i,v)=F(i-1,v)||F(i-1,v-Ci) (v>Ci)
//F(i,v)=F(i-1,v-Ci) (V
//一维优化:F(v)=F(v)||F(v-ci) (v=V....Ci) //此时V要倒着遍历,才能保证计算F(i,v)时,F(i-1,v-ci)还没有被更新
vector<bool>dp(pack+1,false);
dp[0]=true;//容量0 即一个都不选方案,可行
for(int i=0;i<nums.size();++i){
for(int j=pack;j>=nums[i];--j){
dp[j]=dp[j]||dp[j-nums[i]];
}
}
return dp[pack];
}
494 目标和 (01背包-求方案数)
int findTargetSumWays(vector<int>& nums, int S) {
//题目转换:
//假设Sp代表取+号数字 Sn 代表取负号数字,sum=sum(nums)则有:
//Sp+Sn=sum (1)
//Sp-Sn=S (2)
//联合(1)(2)得:Sp=(sum+S)/2
//即转化为背包容量为(sum+S)/2 的01背包问题
//step1:求和sum
int sum=0;
for(auto i:nums) sum+=i;
//step2:一个直观优化,避免很大的S超时。
if(S>sum || S<(-sum)) return 0;
//step3:如果不能平分,只接返回
if((S+sum)%2==1) return 0;
//step4:求解01背包问题
int pack=(S+sum)/2;
//背包容量 pack
//dp[i][j]代表第i个物体的背包容量为j
//F(i,v)=F(i-1,v)+F(i-1,v-ci) v>=Ci
//F(i,v)=F(i-1,v) v
//=>F(v)=F(v)+F(v-Ci) v=V...Ci
vector<int>dp(pack+1);
dp[0]=1;//容量为0时至少存在都不选这个方案
for(int i=0;i<nums.size();++i){
for(int j=pack;j>=nums[i];--j){
//当前选择第i个数字,背包容量为j.
//背包容量恰好等于当前物体数字
dp[j]=dp[j]+dp[j-nums[i]];
}
}
return dp[pack];
}
322 零钱兑换 (完全背包,恰好装满最少数量)
//F(i,v)表示选取第i种物体,背包容积为v时所需最少物体数目
//完全背包 :F(i,v)=min{F(i-1,v),F(i,v-Ci)+1}
//==> F(v)=min{F(v),F(v-Ci)+1}
//进一步优化初始化值,从而避免内部对无效状态的判断
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
if(coins.empty()) return -1;
if(amount==0) return 0;
//-1代表无效方案
//dp[i] 代表容量为i的最小个数
vector<int>dp(amount+1,amount+1);// !!!此处小技巧:将无效值初始化为不可能的最大值
dp[0]=0;//初始条件 amount为0时最小个数为0;
for(int i=0;i<coins.size();++i){
for(int j=coins[i];j<=amount;++j){//另外一个小技巧:将j初始值设为当前物体,避免对是否>=Ci的判断
dp[j]=min(dp[j],dp[j-coins[i]]+1);
}
}
return dp[amount]>amount?-1:dp[amount]; //将无效状态转换为题目的-1
}
};
二维解法
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
if(amount==0) return 0;
if(coins.empty()) return -1;
vector<vector<int>>dp(coins.size()+1,vector<int>(amount+1,-1));
dp[0][0]=0;
for(int i=1;i<=coins.size();++i){
for(int j=0;j<=amount;++j){
if(j==0){ //总数为0所需物体数目为0
dp[i][j]=0;
continue;
}
//不选当前物体
dp[i][j]=dp[i-1][j];
//选当前物体
if(j>=coins[i-1] && dp[i][j-coins[i-1]]>=0){
//注意完全背包问题,即每一种物体数量不限,此处为dp[i][j-coins[i-1]] 而不是dp[i-1][j-coins[i-1]]
dp[i][j]= dp[i][j]>0? min(dp[i][j],dp[i][j-coins[i-1]]+1):dp[i][j-coins[i-1]]+1;
}
}
}
return dp[coins.size()][amount];
}
};
518 零钱兑换 II (完全背包-求方案数)
//F(i,j)=F(i-1,j)+F(i,j-Ci) j>=Ci
//F(i,j)=F(i-1,j) j
//==>F(j)=F(j)+F(j-Ci) j=Ci....V
class Solution {
public:
int change(int amount, vector<int>& coins) {
if(amount==0) return 1;
if(coins.size()==0) return 0;
vector<int>dp(amount+1,0);
dp[0]=1;//容量为0时至少存在一个都不选这个方案,故初始化为1
for(int i=0;i<coins.size();++i){
for(int j=coins[i];j<=amount;++j){
dp[j]=dp[j]+dp[j-coins[i]];
}
}
return dp[amount];
}
};
二维解法
class Solution2 {
public:
int change(int amount, vector<int>& coins) {
if(amount==0) return 1;
if(coins.size()==0) return 0;
sort(coins.begin(),coins.end());
//dp[i][j] 表示对前i个物体,容量为j时的方案数目
vector<vector<int>>dp(coins.size()+1,vector<int>(amount+1,0));
dp[0][0]=1;
for(int i=1;i<=coins.size();++i){
for(int j=0;j<=amount;++j){
//不选第i个物体
dp[i][j]=dp[i-1][j];
//可以选第i个物体
if(j>=coins[i-1]){
dp[i][j]+= dp[i][j-coins[i-1]]; //此处由于数量无限,故是dp[i][j-coins[i-1]]
}
}
}
return dp[coins.size()][amount];
}
};
279 perfect squares(恰好装满最少物体数量)
//状态转移方程:
//完全背包问题:
//dp(i,v)=min{dp(i-1,v),dp(i,v-ci)+1};
//转换为1维数组
//dp(v)=min{dp(v),dp(v-ci)+1}; v=ci....V
class Solution {
public:
int numSquares(int n) {
//找到等待装入背包物体种类,即完全平方数;
vector<int>objects;
for(int i=1;i*i<=n;++i){
objects.push_back(i*i);
}
vector<int>dp(n+1);
for(int i=0;i<dp.size();++i){
dp[i]=i;//初始化为最大可能
}
for(int i=0;i<objects.size();++i){
for(int j=objects[i];j<=n;++j){
dp[j]=min(dp[j],dp[j-objects[i]]+1);
}
}
return dp[n];
}
};
343 Interger break
class Solution {
public:
int integerBreak(int n) {
if(n<=1) return 0;
//转换为完全背包问题
//物体为1....n-1 每种数量不限 背包容量n (最大物体为n-1而不是n可以保证至少两个物体才能装满背包)
//背包价值为背包内物体的乘积,当包内只有一个物体,背包价值就是物体i
//F(i,j)=max{F(i-1,j),Ci} //j==ci
//F(i,j)=max{F(i-1,j),F(i,j-Ci)*Ci } j>Ci
//==>F(j)=max{F(j),F(j-Ci)*Ci}
vector<int>dp(n+1,0);
dp[0]=0;
dp[1]=1;
for(int i=1;i<=n-1;++i){
for(int v=i;v<=n;++v){
if(v==i){
dp[v]=max(dp[v],i);
}else{
dp[v]=max(dp[v],dp[v-i]*i);
}
}
}
return dp[n];
}
};
二维背包问题
474 ones and zeros
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
//二维费用背包,每个物体(string) 有两种体积,0的数目与1的数目。背包的两种容量为m n
//01二维费用背包
//F(i,va,vb)=max{F(i-1,va,vb),F(i-1,va-Cia,vb-cib)}
//==> F(va,vb)=max{F(va,vb),F(va-Cia,vb-Cib)} Va=m....cia,Vb=n...Cib
vector<vector<int>>dp(m+1,vector<int>(n+1,0));
for(int i=0;i<strs.size();++i){
//求当前物体的两种体积
int Cia=0;
for(auto c:strs[i]){
if(c=='0')++Cia;
}
int Cib=strs[i].size()-Cia;
for(int va=m;va>=Cia;--va){
for(int vb=n;vb>=Cib;--vb){
dp[va][vb]=max(dp[va][vb],dp[va-Cia][vb-Cib]+1);
}
}
}
return dp[m][n];
}
};
参考文章:
0-1背包
完全背包
背包九讲
背包细节解析