背包问题---接触动态规划
动态规划的理解
先记住解决动态规划的三个基本要素:
最优子结构:
边界条件
状态转移方程
动态规划要分析,主转台转移和次级状态转移,所以一般至少需要两层循环,若果有多的约束条件可能增加约束,当考虑到当前的一个子问题时,要考虑怎么用前面所有的子问题来解决。
0-1背包问题
题目
有N件物品和一个容量为V的背包。第iii件物品的费用是w[i],价值是v[i],求将哪些物品装入背包可使价值总和最大。
基本思路
东西只有一件,要么放下去要么不放下去。
问题规模分解:主要规模在于物品的个数和背包的容量,阶段性问题就是f[i][j]表示前i件物品恰放入一个容量为j的背包可以获得的最大价值;
来了一个新东西就要考虑遍历背包容量然后更新,新的东西下的在约束条件背包容量下的所有不同的最大值。
则有
int w[] = {3,6,7,12,5,4,9,11,2,3};//w[]表示占用的空间
int val[] = {5,3,8,9,12,4,5,12,5,3};//val[]表示价值
int n=10,v = 20;//注意n位物品个数,也就是问题规模,v表示背包容积,表示限制条件
int dp[11][21];
dp[0][0] = 0;
for(int j=1;j<=v;j++) dp[0][j] = 0;
for(int i=1;i<=n;i++) //外层循环表示物品个数(问题规模,注意这里从表格的下标开始,
//循环体内的物品下标要注意减去1
for(int j=0;j<=v;j++){
if(j>=w[i]) dp[i][j] = max(dp[i-1][j],dp[i-1][j-w[i-1]+val[i-1]];//这里的物品
//要减去1
else dp[i][j] = dp[i-1][j];//上一行的同位置
}
//倒序方法
for(int j=1;j<=v;j++) dp[0][j] = 0;
for(int i=1;i<=n;i++)
for(int j=v;j>=w[i];j--){
dp[i][j] = max(dp[i-1][j],dp[i-1][j-w[i]+val[i]];
}
for(int j=0;j<=w[i]-1;j++)
dp[i][j] = dp[i-1][j];
优化空间
观察状态转移方程,只用到了上一行的状态,所以采用两行就可以,另一方面如果采用滚动数组,同时倒序填充数据,就能保持上一组的数据不会被覆盖。
int dp[v+1] = {0};//注意dp定义的长度是“容积+1”(表示限制条件)
for(int i=0;i<n;i++)//这里下标应从0开始,表示的物品的索引(不要从1开始)
for(int j=v;j>=w[i];j--){
f[j] = max(f[j], f[j - w[i]] + val[i]);
}
一个常数优化空间
观察状态转移方程,
初始化问题
背包不用装满,dp[0][i]=0;
背包要装满,dp[0][0] = 0;dp[0][i] = INT_MIN;
完全背包问题
题目
有N种物品和一个容量为V的背包,每种物品都有无限件可用。第iii种物品的费用是w[i],价值是v[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
基本思路
物品无限使用;新增一个物品,不再是取或不取,而是取0、1、2…n个的问题,按照01背包的思路可以这样写状态转移方程
f[i][j] = max(f[i-1][j],f[i-1][j-k*w[i]]+k*val[i]),0<k*w[i]<=j;
这跟01背包问题一样有O(VN)O(VN)O(VN)个状态需要求解,但求解每个状态的时间已经不是常数了,求解状态f[i][j]的时间是O(V/w[i]),总的复杂度可以认为是O(N∗Σ(V/w[i])),是比较大的。将01背包问题的基本思路加以改进,得到了这样一个清晰的方法。这说明01背包问题的方程的确是很重要,可以推及其它类型的背包问题。
简单的优化方法
完全背包问题有一个很简单有效的优化,是这样的:若两件物品i、j满足w[i]<=w[j]且v[i]>=v[j],则将物品j去掉,不用考虑。这个优化的正确性显然:任何情况下都可将价值小费用高得jjj换成物美价廉的iii,得到至少不会更差的方案。对于随机生成的数据,这个方法往往会大大减少物品的件数,从而加快速度。然而这个并不能改善最坏情况的复杂度,因为有可能特别设计的数据可以一件物品也去不掉。这个优化可以简单的O(N2 )地实现,一般都可以承受。
另外,针对背包问题而言,比较不错的一种方法是:首先将费用大于V的物品去掉,然后使用类似计数排序的做法,计算出费用相同的物品中价值最高的是哪个,可以O(V+N)地完成这个优化。
转为0-1背包
这里需要转换思路想一下,0-1背包的回溯问题分两种情况:不能方向,只考虑i-1个物品就可;能放下,那么考虑max(f[i-1][j], f[i-1][j - w[i]] + val[i]),什么用[i-1]行呢,因为只有一件物品,确定了本物品要放下,那么就要在f中去掉该物品;
那么有
f[i][j]=max(f[i−1][j],f[i][j−w[i]]+v[i])
也能用一维数组优化空间:
for (int i = 1; i <= n; i++)
for (int j = w[i]; j <= V; j++)//顺序开始
f[j] = max(f[j], f[j - w[i]] + v[i]);
多重背包问题
题目
有N种物品和一个容量为V的背包。第i种物品最多有p[i]件可用,每件费用是w[i],价值是v[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
基本算法
这题目和完全背包问题很类似。基本的方程只需将完全背包问题的方程略微一改即可,则有状态转移方程:
f[i][j]=max(f[i−1][j−k∗w[i]]+k∗v[i])∣0<=k<=p[i]&&k*w[i]<=j
转为0-1背包问题
另一种好想好写的基本方法是转化为01背包求解:把第i种物品换成p[i]件0-1背包中的物品,则得到了物品数为Σp[i]的01背包问题,直接求解,复杂度仍然是O(V∗Σp[i])。–暴力转换的思想是取一个一个的取,能够组合成所有的点;
二进制的思想组合思想:–神奇的二进制
我们考虑把第i种物品换成若干件物品,使得原问题中第iii种物品可取的每种策略——取0…p[i]]件——均能等价于取若干件代换以后的物品。另外,取超过p[i]件的策略必不能出现。
方法是:将第iii种物品分成若干件物品,其中每件物品有一个系数,这件物品的费用和价值均是原来的费用和价值乘以这个系数。使这些系数分别为1,2,4,…,2{k−1},p[i]−2{k}+1且k是满足p[i]−2k+1>0p[i]-2^{k}+1>0p[i]−2k
+1>0的最大整数。例如,如果p[i]=13,就将这种物品分成系数分别为1,2,4,61,2,4,61,2,4,6的四件物品。分成的这几件物品的系数和为p[i],表明不可能取多于p[i]件的第i种物品。另外这种方法也能保证对于0…p[i]0…p[i]0…p[i]间的每一个整数,均可以用若干个系数的和表示,这个证明可以分0…2k−1和p[i]-2^{k}-1两段来分别讨论得出,
证明:
这样就将第i种物品分成了O(log(p[i]))种物品,将原问题转化为了复杂度为O(V∗Σlog(p[i]))的01背包问题,是很大的改进。二进制拆分代码如下(三循环)
for(int i = 1;i<=n;i++)
int num = min(p[i],v/w[i]);//去小的
for(int k=1;num>0;k<<1){
if(k>num) k = num;
num = num-k;
for(int j=v;j>=k*w[i];j--){
f[j] = max(f[j],f[j-k*w[i]]+k*val[i];
}
}
二维背包问题–多条件约束
问题
二维费用的背包问题是指:对于每件物品,具有两种不同的费用;选择这件物品必须同时付出这两种代价;对于每种代价都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。设这两种代价分别为代价1和代价2,第i件物品所需的两种代价分别为w[i和g[i]。两种代价可付出的最大值(两种背包容量)分别为V和T。物品的价值为v[i]。
算法思路
增加了一维就增加了一维数据:
设f[i][j][k]表示前i件物品付出两种代价分别为j和k时可获得的最大价值。状态转移方程就是:
f[i][j][k]=max(f[i−1][j][k],f[i−1][j−w[i]][k−g[i]]+v[i])
如前述方法,可以只使用二维的数组:当每件物品只可以取一次时变量jjj和kkk采用逆序的循环,当物品有如完全背包问题时采用顺序的循环。当物品有如多重背包问题时拆分物品。
for(int i=1;i<=n;i++)
for(int j = v;j>=w[i];j--)
for(int k = T;k>=g[j];k--)
f[j][k] = max(f[j][k],f[j-w[i]][k-g[k]]);
总结: