01背包问题
题目:给一个书包,可以放最重为10的物体,给很多物体,有重量,对应的有价值,问在书包可以容纳的情况下,能够获得的最大价值。
这是一个01背包问题,可以对于一个物体有两种状态,放到书包里和不放到书包里。
如: 物体编号: 1 2 3 4 5 6
物体重量: 2 3 1 4 6 5
物体价值: 5 6 5 1 19 7
我们从第一个物体开始考虑,一直到最后一个,每一个有两种状态放在书包里或者放弃掉不放在书包里。
假设现在我们考虑第n个物体,剩余的容量为c,那么用f(n,c)表示当前可以获得的最大价值,那么我们考虑一下第n个物体,是不是当前最大价值只能由两个子问题得来:1、取了第n个物体,考虑第n-1个物体,容量c减去第n个物体的重量,同时加上第n个物体的价值;2没有取第n个物体,那么就简单了,我们只需要考虑第n-1个物体,容量还是c,价值也不用加(没有取嘛)。那么,对于第n(n为任意数)个物体是不是都可以由以前的结果(第n-1个时的结果和当前这个物体的价值来获得)。作为数学表达式可写为:
f(n,c) = max{ f(n-1, c) , f(n-1, c - w) + v} ; w代表第n个物体的重量,v代表第n个物体的价值
我们不妨先用一个矩阵来推导一下(先不考虑程序的实现),我们要使用一个(n + 1) * (c + 1)的矩阵,为什么使用这么大小的呢?可以这样考虑,f(n,c)中,需要n*c,这个没问题吧,因为如果不满足这个没有办法把f(n,c)全部表示出来。为什么要加1呢,因为物体和容量这里都从1开始,我们还有0的情况呢,也就是初始化的情况,最开始我们是都为0的。
好,那么我们看一下过程:
这是初始化,想一下,如果物体个数为0,或者背包的容量为0,那么得到的价值只能是0.
下面我们分析第一行
(1,1)位置,代表有一个容量有一个物体时的最大价值,可以取可以不取第一个。
1、不取。那么能得到的价值就是f(0,1)了,对应就是(0,1)位置的价值,为0
2取。第一个物体的重量为2,二当前的容量为1,所以放不下,也为0.
因此(1,1)位置只能是0.
(1,2)位置,代表有2个容量有一个物体时的最大价值,可以取可以不取第一个。
1、不取。那么能得到的价值就是f(0,2)了,对应就是(0,2)位置的价值,为0
2取。第一个物体的重量为2,二当前的容量为2,可以放下,把这个物体放进去。放进去之后,再看上一个物体,上一个物体为0,容量2-2=0之后,因此为上一个物体的获得的最大价值为f(0,0).因此,当前的最大价值为f(0,0)+5.
比较这两种情况,取两者的最大值,因此得到5.(1,2)位置为5.
后面的情况如以上分析,得到:
看一下整个过程,都是有自己正上面的和上面左边某一个位置的数值得到,比较正上方和左上方(左边移动几个位置位置,上面第一个物体时,左边第二个,后面第二个,第三个。。。物体并不一定,与当前物体的重量有关,当前物体重量是多少,那么就是左边第几个,请从图上考虑一下为什么),获取大的那一个。为什么能从图上得到呢?考虑一下公式:f(n,c) = max{ f(n-1, c) , f(n-1, c - w) + v}。相信能想清楚吧,这里没有任何难度了。
第二个物体,第三个物体的过程与第一行一样。下面我给出整个表格计算完的结果:
为了更清楚上面的过程,我们再来分析一个某个位置如何得来,如(5,6)
1、不取。那么能得到的价值就是f(4,6)了,对应就是(4,6)位置的价值,为16
2取。第5个物体的重量为6,二当前的容量为6,可以放下,把这个物体放进去。放进去之后,再看上一个物体,上一个物体为4,容量6-6=0之后,因此为上一个物体的获得的最大价值为f(4,0).因此,当前的最大价值为f(4,0)+19.
比较这两种情况,取两者的最大值,因此得到19.(5,6)位置为19.
至此,我们的分析过程就完成了,现在想一下,如果我们声明一个二维数组,是不是就可以模仿上面的过程把背包问题解决了呢。
代码就不贴出来了,因为上面的二维数组可以优化,我们最后给一个优化了的代码。
空间复杂度的优化
上面的二维数组是整个计算过程的关键。但是,仔细想一下,当我们计算第5个物体时,只涉及到了数组中这一行和这一行的上一行。这一行是上一行的数据计算得到的,还是公式,两种情况的一种。总之,不管这个物体有没有取,都要去看上一个物体,因此我们可以把真个二维数组优化成只有两行的,例如这两行:
取第4个物体,都是根据第三个物体计算出来的。
到这里已经取得了极大的空间优化。其实,接着认真考虑一下,我们每次都是把上一行的计算,填写到下一行。我们能不能用一行呢?
当然可以,但是这里涉及到一个小技巧。
如果还是从小到大更新(从做到右),我们右边的更新都要依赖左边了,如果从左到右左边的变化了,右边的就没有办法计算了。一次可以从右边向左边更新,左边的更晚更新,不会影响到右边的。这样不就可以用一行解决了吗。举例子来看:
我们要更新(4,10)位置,第4个物体重量为4,如果取的话,那么当前的价值就是f(4-1, 10-4)+1=f(3,6)+1=16+1=17;如果我们在原位置上更新,左边的也不会受到影响。这样就用一行就可以了。
我们再来看一下向左更新终止条件,其实只要到c-w就可以了(w为当前物体的容量,c为一共给的容量)。为什么呢?因为小于这个数的时候,容量放不下当前物体,他取这个物体得到的价值为0(其实取不到,因为放不下),不取这个物体(在上面二维数组中,就是正上方的值)的价值就是当前数组这个位置的数,没有必要更新。因此终止条件就是到c-w就可以了。
其实这是一个动态规划的问题,只不过是最简单的。想学习动态规划的可以去学习一下。
下面贴上优化了空间复杂度的代码:
public class pack { public static void main(String args[]) { Scanner scanner = new Scanner(System.in); int maxValue = scanner.nextInt();//最大容量 int num = scanner.nextInt();//共有的包裹数目 int[] weight = new int[num];//包裹的重量 int[] value = new int[num];//包裹的价值 for (int i = 0; i < num; i++) { weight[i] = scanner.nextInt(); } for (int i = 0; i < num; i++) { value[i] = scanner.nextInt(); } int[] a = new int[maxValue + 1]; for (int i = 0; i < num; i++) {//num为物体的个数,相当于二维数组从上到下 ZeroOnePack(weight[i], value[i], a, maxValue); } System.out.println(a[maxValue]); } public static void ZeroOnePack(int weight, int value, int[] a, int maxValue) { for (int i = maxValue; i >= weight; i--) {//在原数组上更新新值,从后向前,终止条件也不必到0 a[i] = max(a[i], a[i - weight] + value);//a[i]意思是没有取当前的物体 } } public static int max(int a, int b) { return a > b ? a : b; } }