动态规划——背包问题
背包问题是动态规划问题中的一大类。背包问题的大致框架都是:在限定的背包容量 m m m 下,在 n n n 种物品中选出若干件放入背包,使得物品的价值最大。
0/1背包
在背包容量为 m m m 的情况下,在 n n n 种物品中选出若干件(每种物品只有一件)放入背包,使得物品的价值最大。
#include <iostream>
using namespace std;
const int N=37, M=207;
int f[N][M]; //状态数组
//f[i][j]表示当前考虑前i种物品,背包容量为j时的最大价值
int w[N], c[N];
int main()
{
int m, n;
cin>>m>>n;
for(int i=1; i<=n; ++i)
cin>>w[i]>>c[i];
f[0][0]=0; //初始状态的设置
for(int i=1; i<=n; ++i)
for(int j=1; j<=m; ++j)
{
//状态转移方程的设置
if(w[i]<=j)
f[i][j]=max(f[i-1][j], f[i-1][j-w[i]]+c[i]);
else
f[i][j]=f[i-1][j];
}
cout<<f[n][m];
return 0;
}
状态:
1.不选当前的第i种物品:f[i][j]=f[i-1][j]
;
2.选了当前的第i种物品:f[i][j]=f[i-1][j-w[i]]+c[i]
;(在选中当前这个物品之前的状态的最大价值加上当前物品的价值,则表示选中当前物品之后状态的最大价值。)
比较:
状态转移方程:f[i][j]=max(f[i-1][j], f[i-1][j-w[i]]+c[i])
;
优化
我们会发现,每次当前的 f [ i ] f[i] f[i] 状态只与 f [ i − 1 ] f[i-1] f[i−1] 状态有关,所以我们其实只需要每次记录下来前一个状态下的选择情况即可,不需要把 1 i − 2 1~i-2 1 i−2 之间所有的状态都记录下来,这样的我们可以节省一维的空间。
状态转移方程: f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − w [ i ] ] + c [ i ] ) ; f[i][j]=max(f[i-1][j], f[i-1][j-w[i]]+c[i]); f[i][j]=max(f[i−1][j],f[i−1][j−w[i]]+c[i]);
如果直接将数组中的第一维压缩,则状态方程中原本是表示 i − 1 i-1 i−1 状态下的 j − w [ i ] j-w[i] j−w[i] 状态变为了 i i i 状态下的,出现偏差。所以,要保证在计算第 i − 1 i-1 i−1 种状态的时候, j − w [ i ] j-w[i] j−w[i] 这个状态是没有被算过的。所以需要将体积从大到小枚举。
#include <iostream>
using namespace std;
const int N=37, M=207;
int w[N], c[N], f[M];
//将状态数组进行压缩,f[j]表示体积为j时的最大价值
int main()
{
int m, n;
cin>>m>>n;
for(int i=1; i<=n; ++i) cin>>w[i]>>c[i];
f[0]=0;
for(int i=1; i<=n; ++i)
for(int j=m; j>=w[i]; --j) //从大到小枚举
f[j]=max(f[j], f[j-w[i]]+c[i]);
cout<<f[m];
return 0;
}
完全背包(knapsack)
在背包容量为 m m m 的情况下,在 n n n 种物品中选出若干件(每种物品无数件)放入背包,使得物品的价值最大。
//第一版代码:多次0-1背包
#include <iostream>
using namespace std;
const int M=207, N=37;
int w[N], c[N], f[M];
int main()
{
int m, n;
cin>>m>>n;
for(int i=1; i<=n; ++i)
cin>>w[i]>>c[i];
for(int i=1; i<=n; ++i) //第i件物品
for(int j=m; j>=w[i]; --j) //背包容量为j
for(int k=0; k*w[i]<=j; ++k) //当前物品选择k件
f[j]=max(f[j], f[j-k*w[i]]+k*c[i]);
cout<<"max="<<f[m];
return 0;
}
优化
上述方法在数量级较小时可以使用,但三层循环在数量级达到 1000 1000 1000 时就已经会超时了,所以需要对其进行优化。
在 0 − 1 0-1 0−1 背包的代码的优化的过程中我们为了不出现重复的情况,特意将体积倒序枚举了。所以,此处我们再将枚举顺序转成从小到大即可达多次选同一种物品的效果
#include <iostream>
using namespace std;
const int N=37, M=207;
int w[N], c[N], f[M];
int main()
{
int m, n;
cin>>m>>n;
for(int i=1; i<=n; ++i)
cin>>w[i]>>c[i];
for(int i=1; i<=n; ++i)
for(int j=w[i]; j<=m; ++j) //从小到大枚举
f[j]=max(f[j], f[j-w[i]]+c[i]);
cout<<"max="<<f[m];
return 0;
}
多重背包Ⅰ(庆功会(beanfeast))
在背包容量为 m m m 的情况下,在 n n n 种物品中选出若干件(第 i i i 种物品 s [ i ] s[i] s[i] 件)放入背包,使得物品的价值最大。
//代码一:
#include <iostream>
using namespace std;
const int N=507, M=6e3+7;
int v[N], w[N], s[N], f[M];
int main()
{
int n, m;
cin>>n>>m;
for(int i=1; i<=n; ++i)
cin>>v[i]>>w[i]>>s[i];
f[0]=0;
for(int i=1; i<=n; ++i)
for(int k=1; k<=s[i]; ++k)
for(int j=m; j>=k*v[i]; --j)
f[j]=k*max(f[j], f[j-v[i]]+w[i]);
cout<<f[m];
return 0;
}
//代码二:
#include <iostream>
using namespace std;
const int N=507, M=6e3+7;
int v[N], w[N], s[N], f[M];
int main()
{
int n, m;
cin>>n>>m;
for(int i=1; i<=n; ++i)
cin>>v[i]>>w[i]>>s[i];
f[0]=0;
for(int i=1; i<=n; ++i)
for(int j=m; j>=v[i]; --j)
for(int k=0; k<=s[i] && k*v[i]<=j; ++k)
f[j]=max(f[j], f[j-k*v[i]]+k*w[i]);
cout<<f[m];
return 0;
}
两个方法的代码的区别及释义
对比发现,两个版本的代码区别就在于更新状态的循环操作的顺序不同:
//第一版:
for(int i=1; i<=count; ++i) //第i种物品
for(int k=1; k<=y[i]; ++k) //第k个与i相同的物品
for(int j=M; j>=k*w[i]; --j) //背包容量为j时
f[j]=max(f[j], f[j-w[i]]+x[i]);
//第二版:
for(int i=1; i<=count; ++i) //第i种物品
for(int j=M; j>=w[i]; --j) //背包容量为j时
for(int k=1; k<=y[i] && k*w[i]<=j; ++k) //第i种物品选k次
f[j]=max(f[j], f[j-k*w[i]]+k*x[i]);
对于第一个版本,其本质是将 x x x 件相同的物品转换为 x x x 件价值和体积相同的物品,对于每一件物品单独考虑,即更准确地来说: k k k 表达的是当前遍历到第 k k k 个与 i i i 相同的物品,然后去判断这第 k k k 个 i i i 种件物品在不同背包容量 j j j 时是否可以将数组 f f f 更新。所以在状态转移方程中不需要乘 k k k ,因为我们找到的是当前于第 i i i 种物品相同的某一个物品。
而对于第二个版本,其本质是将 x x x 件相同的物品视作一类,即: k k k 表达的是在考虑到第 i i i 种物品且当前背包容量为 j j j 时,对于第 i i i 种物品选择 k k k 次,所以后面还有 一个条件,即 k ∗ w [ i ] < = j k*w[i]<=j k∗w[i]<=j ,该物品选 k k k 次要当前的背包容量要能装下才可以。所以在状态转移方程种需要乘 k k k ,因为是一次性了 k k k 件第 i i i 种物品。
两种方式都是对多重背包的解,但是理解方式不同,代码也略有区别。
多重背包Ⅱ(逃亡的准备(hallows))
对于上述的多重背包问题的解也存在三层循环在数量级较大就会超时的情况,所以需要做出优化。
多重背包的本质是将第 i i i 种物品的 s [ i ] s[i] s[i] 件,拆成 s [ i ] s[i] s[i] 个 1 1 1 的形式,最终变为 0 − 1 0-1 0−1 背包的思想。
将 s [ i ] s[i] s[i] 拆成 s [ i ] s[i] s[i] 个 1 1 1,最终这些 1 1 1 可以组成 1 − s [ i ] 1-s[i] 1−s[i] 之间的任意个数,如果我们可以找到其他几个数,也可以将 1 − s [ i ] 1-s[i] 1−s[i] 之间的任意个数表示出来,只要个数比拆成 s [ i ] s[i] s[i] 个 1 1 1 来的少,就是一个优化。
这个时候我们想到了,利用二进制表示数的思想,例如,三个二进制位( 000 − 111 000-111 000−111)可以表达出 0 − 7 0-7 0−7 之间的所有数,将其对应转化为十进制数就是 1 , 2 , 4 1, 2, 4 1,2,4 这三个数可以表示出 1 − 7 1-7 1−7 之间的所有数。
但如果 s [ i ] s[i] s[i] 不刚好等于 2 2 2 的整数次幂减一,则就会出现本来不存在的数也被组合出来的情况。例如 s [ i ] = 10 s[i]=10 s[i]=10,只能拆出来 1 , 2 , 4 1, 2, 4 1,2,4,还余 3 3 3 的部分如果再用二进制拆出一个 8 8 8 就大于原本的 10 10 10 了。 1 , 2 , 4 , 8 1, 2, 4, 8 1,2,4,8 四个数能组合出来 1 − 15 1-15 1−15 之间的任意数,其中 11 − 15 11-15 11−15 这些数目的是原本不存在的。
所以其实直接在拆到不能再拆的时候将最后的余数也作为拆出的一种情况即可,即 10 10 10 可以拆成 1 , 2 , 4 , 3 1, 2, 4, 3 1,2,4,3。已知 1 , 2 , 4 1, 2, 4 1,2,4 这三个数可以表示出 1 − 7 1-7 1−7 之间的所有数,则加上一个 3 3 3 就也可以表达出 4 − 10 4-10 4−10 之间的数,综合起来就可以表达出 1 − 10 1-10 1−10 之间的任意数了。
将 s [ i ] s[i] s[i] 拆成 s [ i ] s[i] s[i] 个 1 1 1 需要计算 s [ i ] s[i] s[i] 次,而每次拆出一个 2 2 2 的整数次幂需要计算 l o g 2 ( s [ i ] ) log2(s[i]) log2(s[i])次,时间复杂度降低。
#include <iostream>
using namespace std;
const int N=200700, M=507;
int w[N], v[N], f[M];
int main()
{
int n, m;
cin>>n>>m;
int count=0;
for(int i=1; i<=n; ++i)
{
int s, a, b;
cin>>s>>a>>b;
for(int k=1; k<=s; k*=2)
s-=k, w[++count]=k*a, v[count]=k*b;
if(s) w[++count]=s*a, v[count]=s*b;
}
for(int i=1; i<=count; ++i)
for(int j=m; j>=w[i]; --j)
f[j]=max(f[j], f[j-w[i]]+v[i]);
cout<<f[m];
return 0;
}
分组背包(group)
在背包容量为 m m m 的情况下,在 n n n 种物品中选出若干件放入背包,使得物品的价值最大。其中,这 n n n 件物品属于不同组,每组中的物品互斥,如果选当前组中的物品则只能存在一个使得物品的价值最大。
分组背包则需要先按照组去枚举,表示从每组当中选取某一件。
#include <iostream>
using namespace std;
const int N=37;
int w[N], c[N], a[17][37], f[207];//第i组的第j件物品的序号为a[i][j];
int main()
{
int m, n, t;
cin>>m>>n>>t;
int p;
for(int i=1; i<=n; ++i)
{
cin>>w[i]>>c[i]>>p;
a[p][0]++;
a[p][a[p][0]]=i;
}
for(int i=1; i<=t; ++i) //枚举当前组数
for(int j=m; j>=0; --j) //背包容量
for(int k=1; k<=a[i][0]; ++k) //当前组中的第几件物品
if(j>=w[a[i][k]]) f[j]=max(f[j], f[j-w[a[i][k]]]+c[a[i][k]]);
cout<<f[m];
return 0;
}
背包容量j和第k个物品的两层枚举是否能调换位置?
即能否讲上述的更新过程改为:
for(int i=1; i<=t; ++i)
for(int k=1; k<=a[i][0]; ++k)
for(int j=v; j>=w[a[i][k]]; --j)
f[j]=max(f[j], f[j-w[a[i][k]]]+c[a[i][k]]);
答:不能
我们可以暂时先忽略最外层的循环:
//for(int i=1; i<=t; ++i)
for(int k=1; k<=a[i][0]; ++k)
for(int j=v; j>=w[a[i][k]]; --j)
f[j]=max(f[j], f[j-w[a[i][k]]]+c[a[i][k]]);
这时所展现出来的双层循环其实就是一个对于普通的 0 − 1 0-1 0−1 背包的选择。那么对于一个普通的 0 − 1 0-1 0−1 背包问题,对于其所枚举的每件物品都是可能选或不选的,所以如果是当前这个枚举顺序,就不是在 a [ i ] [ 0 ] a[i][0] a[i][0] 件物品中选一件,而变成在这 a [ i ] [ 0 ] a[i][0] a[i][0] 件物品选哪几件了。
回到原本的三重循环,如果是当前这种枚举顺序,表示的是:对于第 i i i 组中的每件物品都可能会将不同的背包容量更新,背包容量最多可能被更新 a [ i ] [ 0 ] a[i][0] a[i][0] 次,即可能选择了 a [ i ] [ 0 ] a[i][0] a[i][0] 件物品,它们之间就没有冲突了。所以应该先枚举背包的容量,再选择当前组当中的某一个物品。
其本质与我们之前讨论多重背包的枚举顺序问题是一致的。可以结合之前的关于多重背包的两个版本的模板一起再理解理解。
二维费用的背包问题(打包(pack))
在背包体积为 m m m,重量为 w w w 的情况下,在 n n n 种物品中选出若干件放入背包,使得物品的价值最大。
限制条件多了一个。所以所以需要考虑两维条件,我们可以直接在原本的背包问题上多枚举一维限制条件。
#include <iostream>
using namespace std;
const int N=387;
int f[N][N]; //f[j][k]表示背包重量为j,体积为k时的最大价值。
int main (void)
{
int G, V, N;
cin>>G>>V>>N;
for (int i=1; i<=N; ++i) //考虑到第i件物品
{
int t, g, v;
cin>>t>>g>>v;
for (int j=G; j>=g; --j) //重量为j
for (int k=V; k>=v; --k) //体积为k时
f[j][k]=max(f[j][k], f[j-g][k-v]+t);
}
cout<<f[G][V];
return 0;
}
混合背包(mix)
将上述所讲过的背包问题结合在一起:对于同一种物品可能不选,可能选一件,可能选 s s s 件,可能选无数件。则根据当前物品的可选数量分情况讨论。
#include <iostream>
using namespace std;
const int N=207;
int f[N];
int main()
{
int m, n;
cin>>m>>n;
for(int i=1; i<=n; ++i)
{
int w, c, p;
cin>>w>>c>>p; //p为可购买的数量
if(p==0)
for(int j=w; j<=m; ++j) //完全背包,顺序遍历背包容量
f[j]=max(f[j], f[j-w]+c);
else for(int j=m; j>=w; --j)
for(int k=1; k<=p && k*w<=j; ++k) //物品数量
f[j]=max(f[j], f[j-k*w]+k*c);
}
cout<<f[m];
return 0;
}
背包问题求方案数(货币系统)
//状态转移方程:f[i][j]=f[i-1][j]+f[i][j-v]
#include <iostream>
using namespace std;
const int N=1e3+7, M=1e4+7;
long long f[N][M];
int main()
{
int n, m;
cin>>n>>m;
f[0][0]=1; //货币价值为0也是一种情况
for(int i=1; i<=n; ++i) //枚举货币种类
{
int x;
cin>>x;
for(int j=0; j<=m; ++j) //枚举面值大小
{
f[i][j]=f[i-1][j]; //先将上一个状态转移过来
if(j>=x) f[i][j]+=f[i][j-x]; //如果当前物品小于当前背包容量则可以放入
}
}
cout<<f[n][m];
return 0;
}
优化
将数组的由原本的二维优化为一维。
#include <iostream>
using namespace std;
typedef long long LL; //重新定义long long的名字
const int M=1e4+7;
LL f[M];
int main()
{
int n, m;
cin>>n>>m;
f[0]=1;
for(int i=1; i<=n; ++i)
{
int x;
cin>>x;
for(int j=x; j<=m; ++j)
f[j]+=f[j-x];
}
cout<<f[m];
return 0;
}
t y p e d e f typedef typedef : t y p e d e f i n i t i o n type~definition type definition的缩写,类型定义