背包问题总结
1.01背包问题
即f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。
则其状态转移方程便是:f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}。
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
{
// 当前背包容量装不进第i个物品,则价值等于前i-1个物品
if(j < v[i])
f[i][j] = f[i - 1][j];
// 能装,需进行决策是否选择第i个物品
else
f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
}
以上方法的时间和空间复杂度均为O(N*V),其中时间复杂度基本已经不能再优化了,但空间复杂度却可以优化到O(V)。
优化为一维:
for (int i = 1; i <= n; i++)
for (int j = m; j >= v[i]; j--)
f[j] = max(f[j], f[j - v[i]] + w[i]);
2.完全背包问题
for(int i = 1 ; i <=n ;i++)
for(int j = 0 ; j <=m ;j++)
{
f[i][j] = f[i-1][j];
if(j-v[i]>=0)
f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);
}
简单有效的优化
完全背包问题有一个很简单有效的优化,是这样的:若两件物品i、j满足c[i]<=c[j]且w[i]>=w[j],则将物品j去掉,不用考虑。
优化为一维:
for(int i = 1 ; i<=n ;i++)
for(int j = v[i] ; j<=m ;j++)//注意了,这里的j是从小到大枚举,和01背包不一样
f[j] = max(f[j],f[j-v[i]]+w[i]);
代码与P01的伪代码只有v的循环次序不同而已。为什么这样一改就可行呢?
首先想想为什么P01中要按照v=V..0的逆序来循环。
这是因为要保证第i次循环中的状态f[i][v]是由状态f[i-1][v-c[i]]递推而来
。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,
依据的是一个绝无已经选入第i件物品的子结果f[i-1][v-c[i]]。
而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i种物品”这种策略时,
却正需要一个可能已选入第i种物品的子结果f[i][v-c[i]],所以就可以并且必须采用v= 0..V的顺序循环。
4.多重背包问题 将转换成01背包问题那一部分优化了就可以了
把第i种物品拆成费用为c[i]*2^k、价值为w[i]*2^k的若干件物品,
其中k满足c[i]*2^k<V。这是二进制的思想,因为不管最优策略选几件第i种物品,
总可以表示成若干个2^k件物品的和。这样把每种物品拆成O(log(V/c[i]))件物品。
int cnt = 0; // 将物品重新分组后的顺序
for (int i = 1; i <= n; i ++)
{
int a, b, s; // a 体积, b 价值, s 每种物品的个数
scanf("%d %d %d", &a, &b, &s);
int k = 1; // 二进制拆分 打包时每组中有 k 个同种物品
while (k <= s) // 即y总说的: 最后一组的物品个数 < 2^(n+1) 1 2 4 8 16 ... 2^n 2^(n+1)
{
cnt ++;
v[cnt] = a * k; // 每组的体积
w[cnt] = b * k; // 每组的价值
s -= k;
k *= 2; // 注意是 k * 2,每次增长一倍,不是k * k
}
if (s > 0) // 二进制拆分完之后 剩下的物品个数分为新的一组
{
cnt ++;
v[cnt] = a * s;
w[cnt] = b * s;
}
}
5.混合三种背包问题
有的物品只可以取一次(01背包),
有的物品可以取无限次(完全背包),
有的物品可以取的次数有一个上限(多重背包)
01背包与完全背包的混合
考虑到在P01和P02中最后给出的伪代码只有一处不同,
故如果只有两类物品:一类物品只能取一次,另一类物品可以取无限次,
那么只需在对每个物品应用转移方程时,根据物品的类别选用顺序或逆序的循环即可,复杂度是O(VN)。
for i=1..N
if 第i件物品是01背包
for v=V..0
f[v]=max{f[v],f[v-c[i]]+w[i]};
else if 第i件物品是完全背包
for v=0..V
f[v]=max{f[v],f[v-c[i]]+w[i]};
再加上多重背包
如果再加上有的物品最多可以取有限次,
那么原则上也可以给出O(VN)的解法:遇到多重背包类型的物品用单调队列解即可。
但如果不考虑超过NOIP范围的算法的话,用P03中将每个这类物品分成O(log n[i])个01背包的物品的方法也已经很优了。
6. 二维费用的背包问题
设这两种代价分别为代价1和代价2,第i件物品所需的两种代价分别为a[i]和b[i]。
两种代价可付出的最大值(两种背包容量)分别为V和U。物品的价值为w[i]。
算法
费用加了一维,只需状态也加一维即可。设f[i][v][u]表示前i件物品付出两种代价分别为v和u时可获得的最大价值。
状态转移方程就是:f [i][v][u]=max{f[i-1][v][u],f[i-1][v-a[i]][u-b[i]]+w[i]}。
如前述方法,可以只使用二维的数组:当每件物品只可以取一次时变量v和u采用顺序的循环,
当物品有如完全背包问题时采用逆序的循环。当物品有如多重背包问题时拆分物品。
物品总个数的限制
有时,“二维费用”的条件是以这样一种隐含的方式给出的:最多只能取M件物品。
这事实上相当于每件物品多了一种“件数”的费用,每个物品的件数费用均为1,可以付出的最大件数费用为M。
换句话说,设f[v][m]表示付出费用v、最多选m件时可得到的最大价值,则根据物品的类型(01、完全、多重)用不同的方法循环更新,
最后在f[0..V][0..M]范围内寻找答案。
另外,如果要求“恰取M件物品”,则在f[0..V][M]范围内寻找答案。
事实上,当发现由熟悉的动态规划题目变形得来的题目时,在原来的状态中加一纬以满足新的限制是一种比较通用的方法。
7.分组背包问题
设f[k][v]表示前k组物品花费费用v能取得的最大权值,则有f[k][v]=max{f[k-1][v],f[k-1][v-c[i]]+w[i]|物品i属于第k组}。
集合划分依据:根据从第i组物品中选哪个物品进行划分.
f[i][j] = max(f[i][j], f[i - 1][j - v[i][k]] + w[i][k]);
for(int i=1;i<=n;i++)
for(int j=0;j<=m;j++)
{
f[i][j]=f[i-1][j]; //不选 不选表示不选第 i 组物品的所有物品,只从前 i−1 组物品里面选
for(int k=0;k<s[i];k++)
if(j>=v[i][k])
f[i][j]=max(f[i][j],f[i-1][j-v[i][k]]+w[i][k]);
}
优化为一维:
for(int i=0;i<n;i++)
for(int j=m;j>=0;j--)
for(int k=0;k<s[i];k++) //for(int k=s[i];k>=1;k--)也可以
if(j>=v[i][k])
f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
8.有依赖的背包问题
我们可以对主件i的“附件集合”先进行一次01背包,得到费用依次为0..V-c[i]所有这些值时相应的最大价值f'[0..V-c[i]]。
那么这个主件及它的附件集合相当于V-c[i]+1个物品的物品组,其中费用为c[i]+k的物品的价值为f'[k]+w[i]。
也就是说原来指数级的策略中有很多策略都是冗余的,通过一次01背包后,将主件i转化为 V-c[i]+1个物品的物品组,
就可以直接应用P06的算法解决问题了。
9.泛化物品 :一个泛化物品就是一个数组h[0..V],给它费用v,可得到价值h[V]。
动态规划的时间复杂度 ≈≈ 问题的总个数 × 问题要做出的选择数
如, 对于 01 背包问题, 问题的总个数为N⋅V (N 为物品个数, V 为背包容量), 问题要做出的选择数为 2(选或不选)
则 01 背包问题的时间复杂度约为 2N⋅V