前情:我最近在做背包问题,但是完全是凭借经验去做,至于其背后的原理了解得不是很深刻。
我的经验如下:
1.关于01背包:二维是正序,一维则是逆序
2.关于完全背包:二维是正序,一维是正序(这就是01背包和完全背包的很重要的一个区别)
3.二维背包的体积需从0开始枚举,例如
for(int i=1;i<=n;i++)
{
for(int j=0;j<=V;j++)
{
if(j>=v[i])
{
f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]);
}
else
{
f[i][j]=f[i-1][j];
}
}
}
关于j和v[i]的大小比较判断是必须要写的,否则可能会出错。所以不要吧一维的东西搞到二维里来。还有一点,关于一维的标准写法是
for(int i=1;i<=N;i++)
{
for(int j=V;j>=v[i];j--)//这里到v[i]就结束是因为剩下的f[j]都是(i-1)时的情况了,相当于不选,所以不用枚举,就算了你枚举了,其实也还是f[j]=f[j],多此一举了属于是//
{
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
}
如上。这就是我的经验。
《1》01背包
1.二维
01背包最特殊的是每个东西最多只能放一次,可以放也可以不放,因此我们很容易得到一个状态转移方程:f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]),然后以此状态转移方程来进行枚举。
for(int i=1;i<=n;i++)
{
for(int j=0;j<=V;j++)
{
if(j>=v[i])
{
f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]);
}
else
{
f[i][j]=f[i-1][j];
}
}
}
当i=1时,属于是放第一个物品了,这是只需满足背包的体积大于物品的体积即可。当i=2时,放第二个物品,那么如果背包体积够用,便是在放进第一个物品后的背包的基础上再放入第二个物品,这便是此时此刻的最大价值。当i=3时,我们此时模拟以下几种情况,背包可以同时容纳1、2、3三种物品,那么此时的最大价值就是再放入前两个物品的基础上再放入第三个物品;倘如背包体积不足,这时可以有不放入第三个物品,但是我们的最终目的是要求最大价值,完全可能有放入1、3或者2、3或者1、2这几种情况啊。所以说对第三个物品进行二维枚举时,f[2][j]可能是放入1和2时的价值,不放1、放入2时的价值,放入1、不放2时的价值,这是因为我们对整个体积进行枚举了嘛(从0到V啦),所以此时我们就可以得知:
(1)第一维的枚举是对第i个物品进行操作。
(2)第二维的枚举是在取决于第i个物品放不放的同时,对前i-1个物品所有可能放入的情况的价值的评判,以此获得最大前i个物品的最大价值,这便是贪心矣!那么问题来了,如果我们对第二维逆序枚举,阁下又该如何应对呢?答案是结果是一样的。因为第二维的本质是对前i-1个物品的价值的探索,所以逆序和正序都是一样的(此时多嘴以下如果压成一维就只能逆序咯,这个我们下面再说)
2.一维
为社么可以压成一维嘞,我们最终要找的是总共的这N个物品,在体积为V的背包下的最大价值,因此最终我们找的是f[N][V]。但是你仔细观察下,你便会发现,f[i][j]在状态转移时两种情况下第一维的转变总是一样的呢(i-1)->i,(ps:这里我抽象了一下下,方便理解),所以我们可以得到,第一维其实是没影响滴,我们通过从1到N的枚举遍历,从f[1]到f[2]到f[3]到....f[N-2]到f[N-1]到f[N],其实就是最大价值的不断递推,所以说既然我们已经遍历到N了,那么其实第一维的影响就没有了。那为啥不用正序写二维呢哥哥?因为啊,当我们枚举到第i个物品时,这时的f[]数组里存着的时背包存放前(i-1)个物品的价值,我们倘如对第二维进行正序枚举,从v[i]开始到V结束,假如某次枚举第i个物品能放,那么就有f[j]=f[j-v[i]]+w[i]了,也就是说f[j]的值更新了,当我们继续正序枚举,就会出现就会形成对价值的误判,f[j]更新过了,表示的是在体积为j的情况下放入第i个物品时的最大价值,当我们对剩下的j+1、j+2、...一直到V进行枚举,假如是x是吧,就可能会出现f[x]=f[x-v[i]]+w[i]的情况,要是x-v[i]恰好等于j情况就糟糕咯,这时第i个物品就放两次咯,形成误判。但当我们用逆序枚举时,是不是就避免了这种情况呢,因为倘若把V首先枚举,后面小的j就不会出现“已经放过一次”的情况了是吧。
《2》完全背包
每个东西可以放无限次。这样就会有
for(int i=1;i<=N;i++)
{
for(int j=0;j<=V;j++)
{
for(int k=0;k*v[i]<=j;k++)//这一维是对放入物品数量的枚举
{
f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
}
}
}
好的,其实和01背包的差别不大嘛,就是多了一层枚举,底层逻辑是差不多的。
你会发现f[i][j]=max(f[i-1][j],f[i-1][j-1*v[i]]+1*w[i],f[i-1][j-2*v[i]]+2*w[i],...,)
你会发现f[i][j-v[i]]=max(f[i-1][j-2*v[i]]+1*w[i],...)("注意这个1“)
那么就可以合并咯,f[i][j]=max(f[i-1][j],f[i][j-v[i]]+w[i]),看到这,想必你已经惊叹于程序员的智慧了吧,从复杂的两个式子简化为一个生动的状态转移方程。通过这个状态转移方程,你就会得到
for(int i=1;i<=N;i++)
{
for(int j=0;j<=V;j++)//注意这里只能正序
{
if(j>=v[i]) f[i][j]=max(f[i-1][j],f[i][j-v[i]]+w[i]);
else f[i][j]=f[i-1][j];
}
}
接下来考虑压到一维,跟那时01背包的解释是差不多的,因为我们是从1到n进行枚举的所以i影响不大,注意完全背包的二维只能正序,这样才有一个物体放了多次,否则就只能放一次。因此可以得到
for(int i=1;i<=N;i++)
{
for(int j=v[i];j<=V;j++)
{
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
}
完毕。睡了。