背包问题
一.
(1)01背包:首先引入问题:什么是01背包?
即我们现在有一个背包,背包有一定的大小,只能装入一定的物品,而你现在面前有一堆 具备价值与体积的物品,你现在的目标就是,如何选择物品,使得装入背包的物品价值最大。(每一件物品只能选择一次)。
怎么选择能使得价值最大?有人会提出使用贪心算法:即每一次选择都使得价值最大,那么装入的总价值就会最大吗? 假如一个小偷有一个可以容纳4千克的背包,但是发现面前只有有3样物品可以偷:台灯(30元,4千克)、音响(20元,3千克)、充电宝(15元,1千克)。问,小偷能够偷到的物品的最大价格是多少(物品的重量不得超过背包的重量)?
如果使用贪心算法,那么第一次选择的是台灯30元,此时背包已经装满,价值为30元。但是很显然,如果小偷选择音响和充电宝的话,价值是35元。所以显然,使用贪心算法是不可取的。
换一种贪心思路呢?即按照性价比进行装填,在这道题看似是可行的,但是实际上01背包是无法使用的。因为一个物品只能选一次,比如背包为60时,物品为2 30 29 30 30 31,按照贪心思路,应该是30+30=60;实际上却应该是30+ 31;
我们用一个表格来详细了解一下背包问题的实质。
计算机逻辑运行
物品\背包 | 0 | 1 | 2 | 3 | 4 |
0 | 0 | 0 | 0 | 0 | 0 |
充电宝 | 0 | 15 | 15 | 15 | 15 |
音响 | 0 | 15 | 15 | 20 | 35 |
台灯 | 0 | 15 | 15 | 20 | 30/35 |
上述表格逻辑价值最大为35元,其中左侧物品的顺序不影响最终结果。
表格逻辑实际上就是作出了一种选择:(1)是否选择这个物品。(2)在选择这个物品情况下的价值和未选这个物品情况下的价值取最大值。
也就是说,我们要考虑,是不是要将这个物品放入,背包,那一种情况的价值最大。
首先,第一重for循环遍历物品个数,第二重循环遍历背包大小。
for(int i=1;i<=n;i++)
for(int j=t;j>=0;j--)
设置一个数组dp[i][j]表示背包大小为j时候的价值最大值
w[i]表示物品的重量。
v[i]表示物品的价值。
dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);表示上述的两个选择的最大值
其中dp数组可以用滚动数组来优化,即填表的时候发现我每一次的选择都是从表格的上一行得来的,因此表格的其他行我们都可以舍弃不要,每次只是覆盖或更新本行(在更新之前,本行和上一行是相同的),即,把二维数组优化为了一维数组,以下所有背包都可以用这个优化
for (int i = 1; i <= n; i++)
for (int j =c[i]; j >= 0; j--)
f[j] = max(f[j], f[j - c[i]] + w[i]);
(2)完全背包
继续拓展,如果装入的次数不限制呢?即每个物品可以选择无限多次。
贪心算法可以解决背包问题,而不能解决0-1背包问题。贪心算法的核心思想是每次都做最优选择,因此在进行贪心算法的时候,首先需要考虑数据按照什么标准排序,贪心的选择按照排序进行,每次选择最优结果,选择后,问题规模下降。因此,贪心算法是自顶向下的算法。
继续用一张表格,改一下数据
有编号分别为a,b,c,d,的五件物品,它们的重量分别是2,3,4,5,它们的价值分别是3,4,5,6,现在给你个承重为8的背包,如何让背包里装入的物品具有最大的价值总和?
物品\背包 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
a | 0 | 0 | 3 | 3 | 6 | 6 | 9 | 9 | 12 |
b | 0 | 0 | 3 | 4 | 6 | 7 | 9 | 10 | 12 |
c | 0 | 0 | 3 | 4 | 6 | 7 | 9 | 10 | 12 |
for (int i = 1; i <= n; i++)
for (int j = 0; j <= c[i]; j++)
f[j] = max(f[j], f[j - c[i]] + w[i]);
所以其实发现,完全背包就是一个没有限制的01背包,只不过优化后的代码需要从正序遍历,因为装入物品不限个,所以修改本行数组的时候,需要对后面的产生影响,反之01背包需要逆序,因为只能装一个,从正序的话根据表格可能会装入多个从而对后面的数据产生影响。
(3) 多重背包
继续拓展,即第i种物品有s【i】个。我们可以把这个问题转换为01背包的问题,即不去管他有多少个,对于每一个物品都进行01背包的选择,直到背包装不下这类物品。
for(int i = 1; i <= N; ++i)
{
for(int j = V; j >= c[i]; --j)
{
for(int k = 0; k <= n[i] && c[i]*k <= j; ++k)
{
dp[j] = max(dp[j], dp[j-k*c[i]] + k*w[i]);
}
}
}
但是,这样时间复杂度就会很大,当数据很大的时候就可能不会通过。
所以我们要对此进行优化。
(1)转化为01背包和完全背包。
(2)二进制优化
二进制优化是一个非常好用,常用的思想。对于一个比较大的数的时候,如果一次次的遍历会超时的话,那么我们就可以用到二进制优化。
例如数论问题中的快速幂,当这个数的幂非常大的时候,一次次遍历会超时,我们就将这个数用二进制表示,转换成2^i,同理,我们也可以将这一类的物品分别分成2^i,就将这一类物品的数量简化几个,然后在进行01背包,就会比较简单了。即分别用1,2,4,8来代表这个数,剩下的余数单独放一个。然后就变成了一个简单的01背包的问题了
int cnt=0;
for(int i=1; i<=N; i++) {
int wi,vi,s;
cin>>wi>>vi>>s;
int k=1;
while(k<=s) {
cnt++;
w[cnt]=wi*k;
v[cnt]=vi*k;
s-=k;
k*=2;
}
if(s>0) {
cnt++;
w[cnt]=wi*s;
v[cnt]=vi*s;
}
}
N=cnt;
for(int i=1; i<=N; i++)
for(int j=V; j>=w[i]; j--)
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
(4)分组背包
有N组物品和一个容量是V的背包。
第i组物品有若干个,同一组内的物品最多只能选一个每件物品的体积是vij,价值是wij。其中i是组号,j是组内编号。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包的容量,且价值总和最大。
for(int i=0;i<n;i++) //从前往后枚举物品
{
int s;
cin>>s;
for(int j=0;j<s;j++) //输入所有物品
cin>>v[j]>>w[j];
for(int j=v;j>=0;j--) //背包体积从大到小枚举
for(int k= 0;k<s;k++)
if(j>=v[k])
f[j]=max(f[j],f[j-v[k]]+ w [k];
}
其本质就是在01背包基础上,对每一个“物品”找最优解。(这里的物品是,第i组物品内,进行选择与不选择进行比较出来的最优解)
(5)二维费用背包
二维费用的背包问题是指:对于每件物品,具有两种不同的费用,选择这件物品必须同时付出这两种费用。对于每种费用都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。
费用加了一维,只需状态也加一维即可。设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采用逆序的循环,当物品有如完全背包问题时采用顺序的循环
然后可以利用上述讲的 将三维数组优化为二维数组。
f[v][u]=max(f[v][u],f[v-a[i]][u-b[i]]+w[i]);
for(int i = 1; i <= N; i++){
//第i件物品所需要的两种费用c,d和它的价值v
int c = cost1[i-1], d = cost2[i-1], v = value[i-1];
for(int j = V; j >= c; j--){
for(int k = U; k >= d; k--){
dp[j][k] = Math.max(dp[j][k], dp[j-c][k-d]+v);
}
}
}