背包体积循环正序和逆序的区别
先说说01背包怎么做,它的代码思路是什么
for(int i=1;i<=n;i++) for(int j=v;j>=v[i];j--) f[j]=max(f[j],f[j-v[i]]+w[i]);
以上是01背包的核心代码(当然v和v数组不能同时出现,只是为了方便表示体积),首先第一层循环很好理解,就是循环一遍n个物品。第二层循环是在循环背包体积,至于为什么倒着循环请看下面的解释,这里先说为什么要循环体积。f[j]数组的含义就是在体积为j的时候,能拿到的最大价值是多少。对于动态规划,那肯定要动,如果当前背包体积小于物品体积,那肯定拿不了,动不了,那就将它设置为边界条件。而开头肯定是背包总体积。而它的动态转移方程也很好理解,当前循环到第i个物品,那就看看到底是拿它好还是不拿好。如果不拿,那当然就不动,是它自己。如果拿了的话,当前背包体积肯定要减去这个物品的体积,同时价值也会加上这个物品的价值,这样dp方程就出来了,就是这两个情况比较大小,然后赋值。
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]);
这是完全背包的代码,与01背包的代码只有一个区别,那就是体积循环的时候倒过来了,是正序循环的。但是其实我第一次学完全背包的时候记的代码是这样的:
for(int i=1;i<=n;i++) for(int j=v;j>=v[i];j--) for(int k=1;k<=j/v[i];k++) f[j]=max(f[j],f[j-k*v[i]]+k*w[i]);
对于初学者而言,第二种完全背包的写法当然是更容易理解的,因为加上这个k的循环,代表了当前背包的体积装1~j/[v[i]]个,j/[v[i]]这就是最大个数,因为你不能装超过背包体积的东西。那么加上这一层循环,装一个就加一个的价值,装两个就加两个的价值,很容易理解,但是这样显然是更慢一些的,因为它比第一种方法多了一层循环。而我们在知道第一个是正确的情况下,看看把循环顺序倒过来,能不能起到k循环个数的作用。
现在我们造一组数据:
5(个数) 10(体积)
1 2(前面是价值,后面是体积)
2 3
3 5
6 8
9 10(虽然我造了五组数据,但其实只需要第一组就能明白其中的奥妙)
现在我们按照完全背包的第一种代码来模拟一下。
当i=1时,就是拿第一件物品的时候拿几件的循环
j=2;j<=10;j++
那么f[2]=max(f[2],f[2-2]+1)=1;这是第一个能拿的体积
f[3]=1;
注意接下来的f[4]!!!!
f[4]=max(f[4],f[4-2]+1)=2;
这时候你会发现,在我们只循环第一件物品的时候,它就相当于拿了两次第一个物品,而价值也相应的乘以了两倍。同理,f[6]=3,f[8]=4.......。而这也是完全背包的性质,一个物品可以拿无数次。
那么我们用同样的一组数据代入01背包的倒序循环体积代码:
f[10]=max(f[10],f[8]+1)=1;
这时候我们惊奇的发现,完全背包刚刚f[10]=5,而01背包f[10]=1,倒着循环体积,那么当前背包的体积就要去找前面的,而前面的都肯定是没有拿过这个物品的,所以可以保证只拿一次。反之,正着循环就会拿好几次,直到你背包装不下了为止。
我总算彻底地理解了背包了ヾ(◍°∇°◍)ノ゙