#背包
背包也算是dp入门吧,反正是必须要学会的。既然有背包九讲,那么我们就一个一个来谈吧先不谈后几讲了。(不要问我为什么,因为我是个菜鸡)。所谓背包,大体的模型就是你有一个背包,这个背包有一个最大的容积V,还有N件物品,你可以按照一些(奇奇怪怪的)规则往里面放进一些(或一个)体积为C[i],价值为W[i]的物品,要求使这些物品的总体积不大于V使总价值最大或最小。
##01背包
作为所有背包的基础,01背包是最简单也最容易懂的(虽然我今天才听懂)。01背包对于每一样物品只能放一件。可是为什么要叫做01背包呢?很简单,对于每一样,只有放和不放两种选择,故0为不放,1为放。
###01背包状态
我们可以用一个二维数组f[i][j]表示前i件物品总体积为j时背包的最大(或最小)价值,最终结果为f[N][V]。
####转移方程
f [ i ] [ j ] = m a x ( m i n ) { f [ i − 1 ] [ j ] , 不取当前物品 f [ i − 1 ] [ j − c [ i ] ] + w [ i ] , 取当前物品 f[i][j] =max(min) \begin{cases} f[i-1][j], & \text{不取当前物品} \\ f[i-1][j-c[i]]+w[i], & \text{取当前物品} \end{cases} f[i][j]=max(min){f[i−1][j],f[i−1][j−c[i]]+w[i],不取当前物品取当前物品
###代码如下
for(int i=1;i<=N;i++)
for(int j=c[i];j<=V;j++)
f[i][j]=max(f[i][j],f[i][j-c[i]]+w[i]);
printf("%d",f[N][V]);
看似已经很完美了,但是。。。
我们不难发现,这样用一个二维数组来存储空间复杂度会非常的大,空间复杂度为(NV),当N和V都超过一万以上差不多就呵呵了,那么我们该怎么解决呢?
###01背包空间优化
通过上面所讲的,我们可以发现放入第i个物品时只与前i-1个物品的状态有关,所以我们很自然的就可以把N的那维去掉,只用f[j]来存储体积为j时第i样物品的最优值,空间复杂度大大降低。不过,需要注意的是,我们不能顺序来推,因为当我们枚举重量为j时,同一件物品重量为前j-1时都被更新了,这样就有可能一件物品会重复选,用状态转移方程来说f[i][j]=max(f[i][j],f[i][j-c[i]]+w[i])。显然,这是不符合01背包的规则的,但逆序求就能有效避免。
###代码如下
for(int i=1;i<=n;i++)
for(int j=V;j>=c[i];j--)
f[j]=max(f[j],f[j-c[i]]+w[i]);
printf("%d",f[V]);
01背包大致就先讲到这里吧,自认为已经讲得非常详细了,起码我觉得我自己时能看懂了。
##完全背包
所谓完全背包,就只把01背包的物品放入规则改动了一下,同一件物品不再是只能放一次,而是可以放无限次。那么我们该怎么做呢?
###冷静分析
还记得上面提到的01背包空间优化吗?我们是用倒序来求的,目的就是为了是同一件物品不重复放入。而现在就是让我们一件物品可以重复放入,那么我们只需要在01背包的模型上正序做即可。
###代码如下
for(int i=1;i<=n;i++)
for(int j=c[i];j<=V;j++)
f[j]=max(f[j],f[j-c[i]]+w[i]);
printf("%d",f[V]);
虽然吧,这段分析相比01背包要水好多,但的确都讲完了,如果你真的要理解完全背包的话,那么就先看懂01背包吧。
##多重背包
现在,我们的物品不再只能放一次或者无数次了,每一样物品我们都只能放入num[i]次。
###胡乱分析(可能有误)
很容易想到的是,我们可以把每一样物品看作是一组,我们只需要再套一层循环枚举每一种物品放入的次数,然后做01背包即可。
###代码
没有,(ノ`Д)ノgun
(好吧,我承认这里我没听,以上纯属瞎搞)这样做真的可以吗?(也许吧)我们假设上面的做法是正确的,那么我们会发现,这样的效率非常低,时间复杂度为O(NVnum),数据稍微一大就GG了,那么就让我们想一想该怎么优化吧。
###二进制优化(不必担心,这个是真懂)
想必二进制拆分应该都会了吧,这里就不细谈了。认真研究过二进制拆分的同学们就会发现当对一个数n进行二进制拆分后,拆分出来的数可以组成1到n的任意一个数,这里就不证明了。所谓二进制优化,就是先把物品件数num[i]进行二进制拆分,用上述思想来代替逐个枚举,时间复杂度为O(NVlog(num[i])),是个log级别的优化。
###代码如下
for(int i=1;i<=N;i++) //二进制拆分
{
p=1;
while(num[i]-p>0)
{
c[++tot]=c2[i]*p;
w[tot]=w2[i]*p;
num[i]-=p;
p*=2;
}
if(num[i]>0)
{
w[++tot]=num[i]*w2[i];
c[tot]=num[i]*c2[i];
}
}
for(int i=1;i<=tot;i++)
for(int j=V;j>=c[i];j--)
f[j]=max(f[j],f[j-c[i]]+w[i]); //01背包
printf("%d",f[V]);
多重背包的二进制优化实现起来也非常简单(我觉得比不优化还简单,这就是我只会优化的理由)。那么多重背包就先讲到这里吧。
上述三种背包算是背包问题中最基础的基础吧,下面我们来讲一些稍微加深点的背包问题吧(下面贴的代码都已01背包为例)
##混合背包
我一开始看到这个感觉非常无脑,直到现在我还是觉得非常无脑。综合上面三种背包的特性,混合背包种的物品有的只有一件,有的有无数件,有的只有num件。
###随便分析
emmmmm。。我觉得这个我就不用怎么讲了吧,只要你上述三种背包已经没有问题了,这个就解决了。只需要先把num件的物品二进制拆分,如果当前物品有无限件那么就按完全背包来做,不然就按01背包来做。
##二维费用的背包问题
同样的,二维费用的背包问题也有三种放法,但不同的是,每一件物品都有两种代价,那么我们又应该怎么解决呢?
###冷静分析
很简单,实际上这只是将状态加了一个,那么我们也只需要将数组多开一维,额外用一层循环k枚举第二个代价,f[i][j][k]表示前i件物品代价1为j,代价2为k时的最优值。
###状态转移
f [ i ] [ j ] [ k ] = m a x ( m i n ) { f [ i − 1 ] [ j ] , 不取当前物品 f [ i − 1 ] [ j − c 1 [ i ] ] [ k − c 2 [ i ] ] + w [ i ] , 取当前物品 f[i][j][k] =max(min) \begin{cases} f[i-1][j], & \text{不取当前物品} \\ f[i-1][j-c1[i]][k-c2[i]]+w[i], & \text{取当前物品} \end{cases} f[i][j][k]=max(min){f[i−1][j],f[i−1][j−c1[i]][k−c2[i]]+w[i],不取当前物品取当前物品
###代码如下
for(int i=1;i<=n;i++)
for(int j=V1;j>=c1[i];j--)
for(int k=V2;k>=c2[i];k--)
f[j][k]=max(f[j][k],f[j-c1[i]][k-c2[i]]+w[i]);
printf("%d",f[V1][V2]);
##分组的背包问题
仍然有三种放法,这一次,我们给出N组物品,每组有tot[i]个物品,但是每组只能取一个(种)物品。
###珂学分析
其实,我们只需要在原有的背包模型上多一重循环来枚举每一组的每一个物品,但是要注意的是,这重循环必须放在最里层,因为这样才能保证每一组只有一个物品被放入。然后,emmm。。。然后就没有然后了。
###代码如下
for(int i=1;i<=n;i++)
for(int j=V;j>=0;j--)
for(int k=1;k<=tot[i];k++)
if(c[i]<=j)f[j]=max(f[j],f[j-c[i][k]]+w[i][k]);
上面所介绍的就是我觉得还比较简单,比较能接受的背包,应该也是普及组的范围内的背包吧。其实原来我背包是一点都不懂的,现在凭着自己的思维也慢慢能懂了,我也是非常惊讶。最后,祝自己:while(1)NOIPRP++;