动态规划中背包问题可以说非常常见,初学以为它很简单,然而很快就在各种题目中崩溃了,打着背包的标签,然而却很难让人想到用背包去做(好吧,是很难让我)。在经过最初的懵逼绝望到现在略有所感,想着赶紧总结一下,也好理一理思绪。
一、01背包
这个没啥好说的,01背包并不难,它的动态转移方程也很好理解。唯一需要注意的就是一般题目是不会问的那么裸的,有时候题目气人就气在你根本就没想到这是可以用01背包做出来的,所以还是一句话,熟能生巧。
1.注意01背包的特征
- 物品可选可不选;
- 每个物品只能选一次。
所以当看到有的题目中的什么什么东西或者是某个变量也具有这种特征时,可能就需要尝试一下01背包的解题思路了。
2.还是说一下它的动态转移方程吧(不然总感觉少点什么)
二维:
dp[i][v] = max(dp[i-1][v],dp[i-1][v-w[i]]+c[i]
一维:
dp[v] = max(dp[v],dp[v-w[i]]+c[i])
(其中dp[i][v]表示前i件物品恰好装入容量为v的背包中所能达到的最大价值)
(一维动态转移方程要注意v需要倒序遍历)
3.dp数组初始化时的技巧
有的时候,并不是说恰好把背包装满了才会得到最大价值,可能它还没有满,但已经有最大价值了。而题目也有可能会有 求背包恰好装满时的最大价值这种问法,此时该怎么解决呢?其实这时只需要在数组的初始化上做点技巧就完事了:
- 求背包恰好装满时的最大价值 :初始化 dp[0]=0,dp[1-n]=-inf。
- 求背包能够达到的最大价值 :初始化 dp[0-n]=0。
怎么理解呢?我举一个例子就一目了然了:
<题目>
背包容量V=5,物品n=3,其中第一件物品的体积和价值分别为 2,2,第二件物品的体积和价值为
3,3,第三件物品的价值为4,6。求如何选取物品,使得背包在恰好装满时能够达到的最大价值。
<分析>
为了便于说明,我特意举了很小的例子,因为要求背包必须装满,我们肯定可以很容易地看出答案是5,采取放第一件物品和第二件物品的策略。放第三件物品虽然能够有更大的价值,但背包不满。
我们让代码来实现:
int n,v,f[N],w[N],c[N];
int main()
{
cin>>n>>v;
for(int i=1;i<=n;i++){
cin>>w[i]>>c[i];
for(int j=v;j>=w[i];j--)
f[j]=max(f[j],f[j-w[i]]+c[i]);
}
for(int i=1;i<=v;i++) cout<<f[i]<<" ";
}
能看懂就好,头文件啥的我就不写了,注意一下最后输出的地方,我特意循环输出f数组的每一个值。
这是01背包的模板代码。
然后呢,运行结果:
3 5
2 2
3 3
4 6
0 2 3 6 6
可以看到,f[5]没有得到题目要求的答案,为什么呢,仔细看每一个f[i]的值就会发现,我们以这种模板求出的f[i]都是最优结果,它不会考虑背包满不满的问题。
为什么呢?因为f[i]初始全是0,所以每一次更新f[i]都会得到一次优化,直到得到最优解。
不妨试一下把f数组初始化改一下?
如下代码:
int n,v,f[N],w[N],c[N];
int main()
{
cin>>n>>v;
for(int i=1;i<=n;i++) f[i]=-1e8;
for(int i=1;i<=n;i++){
cin>>w[i]>>c[i];
for(int j=v;j>=w[i];j--)
f[j]=max(f[j],f[j-w[i]]+c[i]);
}
for(int i=1;i<=v;i++) cout<<f[i]<<" ";
}
运行结果:
3 5
2 2
3 3
4 6
-100000000 2 3 6 5
看见了吧,这种初始化方式得到的f[i]就是刚好满足题目要求的解,为什么会这样?主要是f[1-n]都是-inf,所以只有当刚好满足放入的物品能装满背包时,f[i]才会成功更新,否则f[i]就是-inf+x的状态,肯定还会被之后能够满足装满背包的物品给更新掉。
所以这个问题就说到这里吧?
4.01背包题型总结
#1、求最优值型问题
这个是最常见的一类01背包问题,一般直接套基本模板,或者在模板方程的基础上略微做些改动就好,不多说。
#2、求方案数问题
在遇到一些题目符合01背包的特征,但不是#1的问法,而是要求求出最多或者最少方案数的题目时,可以采取如下这种办法:
便于说明,举个题目的例子:
<题目>
设有1g、2g、3g、5g、10g、20g的砝码若干枚,第一种砝码有a1个,第二种有a2个,以此类推。现每种砝码给出任意数量,求用这些砝码可以称出的不同重量的个数(不包括一个砝码也不用的情况)。
<分析>
首先分析一下这道题为什么可以用01背包来做。
我们把每一个砝码都看作是一个独立的物品,每一个砝码都有它自己的重量,在求方案数时,每一个砝码都是可选可不选的,另外,因为每一个砝码都是独立的个体,所以只能放一次,物品的个数就是全部砝码的总和。这刚好满足01背包。
然后来想一想怎么做。这个不同于大多数01背包的题目,它不是要求最优解,每一个物品也只有重量没有价值,甚至这道题目连背包都不具备。
怎么做呢?设f[i][j]为前i件物品,体积为j的方案总数。
当第i件物品不选时:f[i][j] = f[i-1][j]
当选第i件物品时 :f[i][j] = f[i-1][j-w[i]]
显然如上两个方程都是一种可行的方案,于是:f[i][j] = f[i-1][j] + f[i-1][j-w[i]]
降维:f[j] = f[j] + f[j-w[i]],即 f[j] += f[j-w[i]]
于是我们的方程就这样出来了,这个方程也可以作为一个模板记下来。
之后的解题思路就和普通01背包的差不多了,代码就不贴了。
注意点有二:
一是这样求出f[]数组之后,还需要把每一种容量的方案数全部加起来才能得到总的方案数;
二是初始化时f[0]=1,因为什么都不选也是一种方案。
#3、留坑,以后遇到其它题型再来更新。
5.01背包例题实战
一些01背包的题目,可以练练手:
(题目均来自洛谷)
01背包求最优值型:
P1048 采药:https://www.luogu.org/problem/P1048 (裸01背包)
P1049 装箱问题:https://www.luogu.org/problem/P1049 (裸01背包)
P2663 越越的组队:https://www.luogu.org/problem/P2663 (裸01背包)
P1060 开心的金明:https://www.luogu.org/problem/P1060 (01背包模板简单变形)
P2430 严酷的训练:https://www.luogu.org/problem/P2430 (01背包水题)
P1507 NASA的食物计划:https://www.luogu.org/problem/P1507 (简单变形,三维化二维)
P1910 L国的战斗之间谍:https://www.luogu.org/problem/P1910 (三维化二维01背包,与P1507如出一辙)
P1734 最大约数和:https://www.luogu.org/problem/P1734 (半裸01背包)
P1802 五倍经验日:https://www.luogu.org/problem/P1802 (01背包变形)
P1877 音量调节:https://www.luogu.org/problem/P1877 (01背包变形)
01背包求方案数型:
P2347 砝码称重:https://www.luogu.org/problem/P2347
二、完全背包
完全背包和01背包的不同之处就在于完全背包里面每一种物品都可以放无数次。
1.完全背包方程的推导之路
在完全背包问题中,尽管物品可以无数次的选取,但背包的容量总是有限的,既然背包不能无底线的接纳,那就存在一个临界点。
比如说背包体积为v,物品有m件,每一件的体积为w[i],价值为c[i],那么当放第i件物品时,虽然我们可以在理想状态下放无数次,但其实最多也就只能放 v/w[i] 次而已,再结合推导01背包时的思路,于是我们得到了完全背包的初始方程:
f[i][j] = max(f[i-1][j],f[i-1][j-k* w[i]]+k*c[i])
(1<=k<=v/w[i])
再想,f[i-1][j]意为不取第i件物品,它正好对应k取0,于是又有:
f[i][j]=max(f[i-1][j-k* w[i]]+k*c[i]) (0<=k<=v/w[i])
这个式子推出来了咱先把它放过去,记作式二吧,先回过头来看式一,式一其实非常明显,它就是比较 不放第i件物品,以及放第i件物品k件 这两种情况下哪个有最优值。
其中,不放物品的话那就直接f[i-1][j],没什么好说的,而放物品又有许多小情况,再摘出来分析:
既然要放,最少肯定要放一件,一件是跑不了的,因此先把这确定的一件放进去,之后再放多少件才会有最优值再判断就好,于是得到:
f[i][j]=max(f[i-1][j],max(f[i-1][(j-w[i])-k* w[i]]+k*c[i])+c[i]) (k>=0)
这时候终于轮到式二出场了,有没有感觉这个新推导出来的臭长的方程的后半部分与式二很像呢,对,它不就是式二的j又多减了个w[i]、多加了个c[i]嘛。
即:
f[i][j-w[i]]+v[i]=max(f[i-1][(j-w[i])-k* w[i]]+k*c[i])+c[i]
费劲周折,终于得到完全背包的最终方程:
f[i][j]=max(f[i-1][j],f[i][j-w[i]]+c[i])
当然,也可以降维:
f[j]=max(f[j],f[j-w[i]]+c[i])
降维后你会发现它和01背包的一维方程完全一样,不过完全背包的j的遍历只能正序着来,所以还是有些不同之处的。
2.完全背包的初始化
完全背包的dp数组初始化完全和01背包的一样,看题目怎么要求了,这里就不再重复说明了。
3.完全背包的题型
#1、求最优解
#2、求方案数
基本思路与01背包一样,甚至模板方程都可以共通。
#3、留坑,以后遇到其它题型再来更新。
4.一些完全背包的题目
完全背包求最优值:
P1616 疯狂的采药:https://www.luogu.org/problem/P1616 (裸完全背包)
P2772 总分:https://www.luogu.org/problem/P2722 (裸完全背包)
P1679 神奇的四次方数:https://www.luogu.org/problem/P1679 (半裸完全背包)
完全背包求方案数:
P1832 A+B Problem:https://www.luogu.org/problem/P1832
三、多重背包
多重背包是建立在01背包和完全背包的基础上的,01背包要求物品最多只能选一次,完全背包要求物品可选无数次,而多重背包则要求物品可选指定次数。
1.多重背包dp方程
有过完全背包方程推导经验的我们再来推导多重背包就十分简单,它不就是指定了一个次数k么,直接给出方程:
f[i][j] = max(f[i-1][j-k* w[i]]+k*c[i])
(0<=k<=r[i] && 0<=k*w[i]<=j,r[i]表示第i种物品指定的最多可选次数)
压缩:
f[j] = max(f[j-k* w[i]]+k*c[i])
2.多重背包问题转换
多重背包因为其物品的选取介于01背包和完全背包之间的特性,因此很多时候可以把多重背包问题转换为01背包或者完全背包问题。
当w[i]*r[i]>=v时,可以把第i类物品看作是无限的,即得到一个完全背包问题。
当w[i]*r[i]<v时,可以把第i类物品拆分,拆分为一个又一个独立的物品,那么对于第i类物品而言,这是一个01背包问题。
在拆分策略上,如果数据很大,把每一种物品拆分为r[i]个新物品的代价是很大的,这很有可能导致复杂度太高,许多题目会因此而无法通过,这时可以采用二进制拆分法优化。
3.二进制拆分法优化
任何一个正整数n,都可以被分解成1,2,4,…2^(k-1),n - 2^k +1(k是满足n-2^k
+1>0的最大整数)的形式,且1到n之间的任何一个整数都可以唯一的表示成1,2,4,…2^(k-1) , n - 2^k +1中某几个数的和的形式。
鉴于此,可以把r[i]以二进制拆分法分解,例如r[i]=13,则拆分为1,2,4,6四件物品,即拆分为w[1]=w[i]*1,c[1]=c[i]*1;w[2]=w[i]*2,c[2]=c[i]*2…
这样,就把n件物品拆分为了至多log n 物品。
复杂度也就随之降下去了。
4.多重背包解题模板
#1 背包转换为01背包且不优化
for(int i=1;i<=n;i++) cin>>w[i];//物品体积
for(int i=1;i<=n;i++) cin>>c[i];//物品价值
for(int i=1;i<=n;i++) cin>>r[i];//每种物品的个数
for(int i=1;i<=n;i++) //遍历每一种物品
for(int j=0;j<=r[i];j++) //对每种物品遍历
for(int k=v;k>=w[i];k--) //当作01背包处理
f[k]=max(f[k],f[k-w[i]]+c[i])
#2 二进制优化后的模板
for(int i=1;i<=n;i++) cin>>w[i];//物品体积
for(int i=1;i<=n;i++) cin>>c[i];//物品价值
for(int i=1;i<=n;i++) cin>>r[i];//每种物品的个数
for(int i=1;i<=n;i++)
{
if(w[i]*r[i]>=v) //满足完全背包就用完全背包的方程
{
for(int k=w[i];k<=v;k++)
f[k]=max(f[k],f[k-w[i]]+c[i]);
}
else //转换为二进制优化后的多个01背包
{
int t;
for(t=1;t*2<=r[i]+1;t*=2) //二进制优化
for(int k=v;k>=w[i]*t;k--)
f[k]=max(dp[k],dp[k-w[i]*t]+t*c[i]);
t=r[i]+1-t;
for(int k=v;k>=w[i]*t;k--)
f[k]=max(dp[k],dp[k-w[i]*t]+t*c[i]);
}
}
5.多重背包例题
P1964 卖东西:https://www.luogu.org/problem/P1964
P2347 砝码称重:https://www.luogu.org/problem/P2347
P1757 通天之分组背包:https://www.luogu.org/problem/P1757
四、待更新
在刷题的过程中其实还遇到了一些不同于以上三种基本背包的题目,比如说二维费用的背包或者混合背包,但接触的还不够多,等系统学习了以后再来补充总结。