背包问题是一类非常经典的动态规划问题,其主要表现为要在一定体积(或是一定时间之类的)要求下拿一定物品,求最多的价值,有着多种形式,这里来讲几种常见的背包问题
01背包
题目
将 N 件价值 v[ i ],体积为 c[ i ]的物品放入体积为 V 的背包中,如何使价值最大
<例题>
方法一
基本思路
01背包是背包问题最基础的形态,对于每件物品都只有放或不放两种情况
在这里定义 f[ i ][ j ],表示前 i 件物品占用 j 的容量(这里的占用并不一定是全装满)所得到的最大价值
故得状态转移方程式如下:
f [ i ][ j ]=max( f[ i - 1 ][ j ] ,f[ i - 1 ][ j - c[ i ] ] + v[ i ] )
即判断第 i 件物品是否要放在占用 j 的容量的地方时,只需考虑第 i - 1 件物品放完后的状态。
在 j 不放第 i 个物品时价值为最大f[ i - 1 ][ j ],在 j 放第 i 个物品时最大价值为f[ i - 1 ][ j - c[ i ] ] + v[ i ](将物品放入占空间 j-c[ i ] 的情况,用那种情况下的最大价值加上第 i 个物品的价值)。因此前 i 个物体的最大价值就是二者的最大值
代码实现
#include<iostream>
using namespace std;
const int N=1e5;
int c[N],v[N];
int f[N][N];
int main()
{
int i,j,k,n,m;
cin>>n>>m;
for(i=1;i<=n;i++) cin>>c[i]>>v[i];
for(i=1;i<=n;i++)
{
for(j=1;j<=m;j++)
{
if(j>=c[i])//如果空间不够放入物体,f[i][j]=f[i-1][j]
{
f[i][j]=max(f[i-1][j],f[i-1][j-c[i]]+v[i]);
}
else
{
f[i][j]=f[i-1][j];
}
}
}
cout<<f[n][m];
return 0;
}
方法二(空间优化)
基本思路
上一种方法的时间复杂度基本上已经达到最优,但在空间上仍旧有待优化
因为 f 数组每次更新都只需考虑上一轮更新的结果,所以我们可以将 f 数组缩减到一维,即根据自己更新自己
此时的状态转移方程式:
f [ j ] =max( f [ j ],f [ j - c [ i ] ] + v [ i ] )
这时的核心代码为:
for(i=1;i<=n;i++)
for(j=m;j>=v[i];j--)
f[j]=max(f[j],f[j-v[i]]+w[i]);
注意,自我更新时要采用倒序的方式,防止当前更新所用到的值是这一轮所更新过的,这点将在后文中详细讲到
(在这里)
代码实现
#include<iostream>
using namespace std;
const int N=1e5;
int c[N],v[N];
int f[N];
int main()
{
int i,j,k,n,m;
cin>>n>>m;
for(i=1;i<=n;i++) cin>>c[i]>>v[i];
for(i=1;i<=n;i++)
{
for(j=m;j>=c[i];j--)//一定要注意是倒序!!!!
{
f[j]=max(f[j],f[j-c[i]]+v[i]);
}
}
cout<<f[m];
return 0;
}
完全背包
题目
与01背包基本相同,只不过每件物品有无穷多个
<例题>
方法一
基本思路
因为物品有无穷多个,所以物品可以取0个、1个、2个…,只要总体积不超过 v 就可以一直放下去。
仍按照01背包的第一种思路,得状态转移方程式:
f [ i ][ j ]=max( f [ i - 1 ][ j ],f [ i - 1 ] [ j - k *c [ i ] ] + k *v [ i ]) || ( 0 ≤ k *c [ i ] ≤ j )
由此得具体更新操作:
for(i=1;i<=n;i++)
for(j=1;j<=m;j++)
for(k=0;k<=j/v[i];k++)
f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
显然,这种方式的时间复杂度过高,仍有待简化
代码实现
#include<iostream>
using namespace std;
int n,m;
int c[1005],v[1005],f[1005][1005];
int main()
{
int i,j,k;
cin>>m>>n;
for(i=1;i<=n;i++) cin>>c[i]>>v[i];
for(i=1;i<=n;i++)
{
for(j=1;j<=m;j++)
{
for(k=0;k<=j/c[i];k++)
{
f[i][j]=max(f[i][j],f[i-1][j-k*c[i]]+k*v[i]);
}
}
}
cout<<f[n][m];
return 0;
}
方法二(时间、空间优化)
基本思路
先前在01背包的空间优化中提到,01背包用倒序更新是为了防止更新所用到的值是被更新过的,而完全背包正好可以利用正序会更新被更新过的值这一特点,去更新已经更新过的值,从而达到对原来的代码进行优化的目的
状态转移方程式如下:
f [ j ] = max( f [ j ],f [ j - v [ i ] ] + w [ i ] );
更新操作转变为:
for(i=1;i<=n;i++)
{
for(j=c[i];j<=m;j++)
{
f[j]=max(f[j],f[j-c[i]]+v[i]);
}
}
这种方法能有效的将时间复杂度压缩至O(n*m),空间复杂度压缩至V(m)
代码实现
**#include<iostream>
using namespace std;
int n,m,c[1005],v[1005];
int f[1005];
int main()
{
int i,j,k;
cin>>n>>m;
for(i=1;i<=n;i++) cin>>c[i]>>v[i];
for(i=1;i<=n;i++)
{
for(j=c[i];j<=m;j++)//额外注意这里是正序,和01背包做好区分
{
f[j]=max(f[j],f[j-c[i]]+v[i]);
}
}
cout<<f[m];
return 0;
} **
其他的可用小技巧(代码示例中未使用)
因为多重背包的物品可以无限使用,所以当一个物品的体积大于另一个物品且价值小于那个物品时,体积大且价值小的物品可以被直接舍去,但在题内可能会被卡数据,可酌情使用
01背包与完全背包的更新顺序
当需要对背包进行更新时,背包的更新顺序将决定更新所用到的值是否被更新过
01背包更新时,因为其状态转移方程式更新所用到的是上一轮更新的结果,所以需要避免更新所用的值被更新而导致同一件物品被多次放置,故而采取先更新 j 取较大值的倒序
多重背包更新时,因为物品可以无限使用,所以无需担心物品被多次使用,而且需避免一件物品只能被用一次,故而采用正序
多重背包
题目
基本与01背包相同,但每件物品可以使用 s 次
<例题>
方法一
基本思路
从最基本的方面看,多重背包可以被看作有 s[ i ] 个 i 物品的01背包
由此得状态转移方程式:
f [ j ] = max( f [ j ],f [ j - c [ i ] *k ] + v [ i ] *k) || ( k <= s [ i ] )
只需在01背包代码基础上加一层循环即可
代码实现
#include<iostream>
using namespace std;
int c[1005],v[1005],s[1005];
int f[1005];
int main()
{
int n,m;
int i,j,k;
cin>>n>>m;
for(i=1;i<=n;i++) cin>>c[i]>>v[i]>>s[i];
for(i=1;i<=n;i++)
{
for(j=m;j>=1;j--)
{
for(k=1;k<=s[i];k++)
{
if(j>=c[i]*k) f[j]=max(f[j],f[j-c[i]*k]+v[i]*k);
}
}
}
cout<<f[m];
return 0;
}
方法二(二进制优化时间复杂度)
基本思路
一般情况下上一种方法就够用了,这种方法不强求掌握
因为上一种方法直接采用01背包的方法,我们可以通过将 s 转为二进制再分别乘上 c 与 v 的方法将 s 个数组合成若干个可以组成不大于 s 的任意 2x
具体组合方法:
for(i=1;i<=n;i++)
{
for(j=1;j<=s[i];j*=2)
{
cnt++;
c1[cnt]=c[i]*j;
v1[cnt]=v[i]*j;
s[i]-=j;
}
if(s[i]>0) //赋值为体积为c[i]*s[i],价值为v[i]*s[i]的物品
{
cnt++;
c1[cnt]=c[i]*s[i];
v1[cnt]=v[i]*s[i];
}
}
代码实现
#include<iostream>
using namespace std;
int n,m;
int c[12000],v[12000],s[12000];
int c1[12000],v1[12000];
int f[12000];
int main()
{
int i,j,k,cnt=0;
cin>>n>>m;
for(i=1;i<=n;i++) cin>>c[i]>>v[i]>>s[i];
for(i=1;i<=n;i++)
{
for(j=1;j<=s[i];j*=2)
{
cnt++;
c1[cnt]=c[i]*j;
v1[cnt]=v[i]*j;
s[i]-=j;
}
if(s[i]>0)
{
cnt++;
c1[cnt]=c[i]*s[i];
v1[cnt]=v[i]*s[i];
}
}
for(i=1;i<=cnt;i++)
{
for(j=m;j>=c1[i];j--)
{
f[j]=max(f[j],f[j-c1[i]]+v1[i]);
}
}
cout<<f[m];
return 0;
}
分组背包
题目
基本同01背包,但物品分为 k 组,每组中物品相互冲突
<例题>
具体做法
基本思路
对每一组数,可选择某一个物品,也可一个都不选
因而我们可以对每一组中的每个数进行更新,最后取到最优情况
状态转移方程式如下:
f [ j ] = max( f [ j ],f [ j - c [ i ][ k ] ] + v[ i ][ k ])
即对每组进行分别操作
代码实现
#include<iostream>
using namespace std;
int n,m;
int s[1005],c[1005][1005],v[1005][1005];
int f[1005];
int main()
{
int i,j,k;
cin>>n>>m;
for(i=1;i<=n;i++)
{
cin>>s[i];
for(j=1;j<=s[i];j++)
{
cin>>c[i][j]>>v[i][j];
}
}
for(i=1;i<=n;i++)
{
for(j=m;j>=1;j--)
{
for(k=1;k<=s[i];k++)
{
if(c[i][k]<=j)
f[j]=max(f[j],f[j-c[i][k]]+v[i][k]);
}
}
}
cout<<f[m];
return 0;
}
结束语
以上就是有关常见的背包问题的全部分析及讲解了,祝大家学业有成,工作顺利