背包算法研究

来源:互联网 发布:淘宝成交排名 编辑:程序博客网 时间:2024/06/06 21:37

背包算法

问题描述

假定背包的最大容量为W,N件物品,每件物品都有自己的价值和重量,将物品放入背包中,使得背包内物品的总价值最大。

有这样一个场景——小偷在屋里偷东西,他带着一只背包,屋子里物品数量有限——每件物品都有一定的重量和价值——珠宝重量轻但价值高,桌子重但价值低,最重要的是小偷背包容量有限.很显然,他不能把桌子分成两份或者带走珠宝的3/4.对于一件物品他只能带走或不带走。

实例:

Knapsack Max weight : W = 10 (units)  //背包总重量为10Total items         : N = 4           //最多容纳4件Values of items     : V[] = {10,40,30,50} Weight of items     : w[] = {5,4,6,3}

从实例数据大致估算一下,最大重量为10时,背包能容纳的物品最大价值为50+40=90,重量为7.

解决方法

最佳的解决方法是使用动态规划——先得到该问题的局部解然后扩展到全局问题解。

构建物品X在不同重量时的价值数组V(Value数组)

V[N][W] = 4 rows * 10 columns

该矩阵中的每个值的求解都代表一个更小的背包问题。

初始情况一:对于第0列,它的含义是背包的容量为0,此时物品的价值?没有,因此,第一列都填充0.

初始情况二:对于第0行,它的含义是屋内没有物品。那么没有任何物品的背包里的价值多少呢?还是没有!所有都是0。(WeightW,表示总重为W时的情况)

步骤

1、现在,开始填入数组每一行的值。第1行第1列表示什么含义呢?对于第一个物品,可以把重量为1的该物品放入背包吗?不行。第一个物品的重量是5,大于0。因此,填入0。实际上直到第5列(重量5)之前都应该填入0。

2、对于第1行的第5列(重量5),意味着将物品1放入背包。填入10(注意,这是Value数组):

3、继续,对于第6列,我们可以再放入重量为1(重量值-物品的重量)的物品吗。我们现在只考虑物品1。由于我们加入物品1之后就不能再加入额外的重量,可以很直观地看到其余的列都应该还是相同的值。

4、接着,有意思的事情出现,在第3行第4列,此时重量为4。作如下判断:

  1. 可以放入物品2吗?——可以,物品2的重量为4.
  2. 不加入物品2的话,当前已有物品的重量的Value值是否最大——查看相同重量时的前一行的值。
  3. 前一行的值为0,重量4时不能放入物品1.
  4. 在这个重量时可以放入两件物品使得价值最大码?——不能,此时重量减去物品2的重量后为0.

为什么是前一行

简单来说,重量为4的前一行的值本身就是个更小的背包问题解,它的含义是到该重量时背包内物品的最大价值(通过遍历物品得到)。

举个例子:

  1. 当前物品价值 = 40
  2. 当前物品重量 = 4
  3. 剩余重量 = 4-4 = 0
  4. 查看上面的行(物品1或者其余行的值)。剩余容量为0时,可以再容纳物品1吗?对于该给定的重量值上面的行还有任何值吗?

计算过程如下:

1) 计算不放入该物品时该重量的最大价值:

previous row, same weight = 0=> V[item-1][weight]

2) 计算当前物品的价值 + 可以容纳的剩余重量的价值

Value of current item+ value in previous row with weight 4 (total weight until now (4) - weight of the current item (4))=> val[item-1] + V[item-1][weight-wt[item-1]]

找到二者之中的最大值40(0和40)。

3) 下一次最重要的位置为第2行第9列。意味着此时重量为9,放入两件物品。根据示例数据现在可以放入两件物品。我们作了以下判断:

The value of the current item = 40The weight of the current item = 4The weight that is left over = 9 - 4 = 5Check the row above.  At the remaining weight 5, are we able to accommodate Item 1.

计算如下:

不加入该物品时该重量的最大价值:
previous row, same weight = 10

计算当前物品的价值+可以容纳的剩余重量的价值

Value of current item (40)+ value in previous row with weight 5 (total weight until now (9) - weight of the current item (4)) = 10

10 vs 50 = 50。

解决了所有的子问题之后,返回V[N][W]的值——4件物品重量为10时:

复杂度

解法的复杂度非常直观。在N次循环中有W次循环 => O(NW)

实现

Java代码实现:

class Knapsack {    public static void main(String[] args) throws Exception {        int val[] = {10, 40, 30, 50};        int wt[] = {5, 4, 6, 3};        int W = 10;        System.out.println(knapsack(val, wt, W));    }    public static int knapsack(int val[], int wt[], int W) {        //Get the total number of items.         //Could be wt.length or val.length. Doesn't matter        int N = wt.length;         //Create a matrix.         //Items are in rows and weight at in columns +1 on each side        int[][] V = new int[N + 1][W + 1];         //What if the knapsack's capacity is 0 - Set        //all columns at row 0 to be 0        for (int col = 0; col <= W; col++) {            V[0][col] = 0;        }        //What if there are no items at home.          //Fill the first row with 0        for (int row = 0; row <= N; row++) {            V[row][0] = 0;        }        for (int item=1;item<=N;item++){            //Let's fill the values row by row            for (int weight=1;weight<=W;weight++){                //Is the current items weight less                //than or equal to running weight                if (wt[item-1]<=weight){                    //Given a weight, check if the value of the current                     //item + value of the item that we could afford                     //with the remaining weight is greater than the value                    //without the current item itself,注意区分val[item-1]和V[item-1][weight]的取值,val[item-1]值是数组中的值,代表当前项值,V[item-1][weight]的值是表格中,纵向上,当前项的前一项的值。                    V[item][weight]=Math.max (val[item-1]+V[item-1][weight-wt[item-1]], V[item-1][weight]);                }                else {                    //If the current item's weight is more than the                    //running weight, just carry forward the value                    //without the current item                    V[item][weight]=V[item-1][weight];                }            }        }        //Printing the matrix        for (int[] rows : V) {            for (int col : rows) {                System.out.format("%5d", col);            }            System.out.println();        }        return V[N][W];    }}

动态规划论述

动态规划(Dynamic programming),简称DP,是指对某一类问题解决方法。重点在于如何鉴定“某一类问题”是动态规划可解的,而不是纠结解决方法是递归还是递推。

怎么鉴定DP可解的问题,可以从计算机的工作方式说起。计算机本质是一个状态机,内存中存储的所有数据构成了当前的状态,CPU只是利用当前的状态计算出下一个状态(不要纠结硬盘之类的外部存储,就算考虑他们也只是扩大了状态的存储容量,并不能改变下一个状态只能从当前状态计算出来这一条铁律)。

当你企图用计算机解决一个问题,其实就是在思考如何将这个问题表达成状态(用哪些变量存储哪些数据),如何在状态中转移(怎么根据一些变量计算出另一些变量)。所谓的“空间复杂度”,就是为了支持你的计算所必须存储的状态最多有多少,所谓“时间复杂度”,就是从初始窗台到最终状态,中间需要多少步。

比如:我想计算第100个斐波那契数列,每一个斐波那契数列就是这个问题的一个状态,每次求解一个新数,只需之前的两个状态。所以同一时刻,最多只需保存两个状态,空间复杂度就是常数。每计算一个新状态所需要的时间也是常数,且状态是线性递增的,所以时间复杂度也是线性的。

这种状态计算很直接,只需按照固定模式从旧状态计算新状态 a[i]=a[i-1]+a[i-2],无需考虑更多状态,也不需要对旧状态进行选择。这样的解法,我们称之为“递推”。

斐波那契数列的例子过于简单,以至于忽视了阶段的概念,所谓阶段是指随着问题的解决,在同一时刻可能得到不同的状态集合。斐波那契数列中,每一步会计算一个新数字,所以每个阶段只有一个状态。

想像另一个问题场景:假如把你放在一个围棋棋盘的某一点,你每一步只能走一格,因为你可以东西南北随便走,所以当你同样走几步可能会处于很多个不同的位置。从头开始走了几步就是第几个阶段,走了n步可能处于的位置称为一个状态,走了这n步所有可能到达的位置集合就是这个阶段所有可能的状态。

问题来了,有了阶段之后,计算新状态可能会遇到各种奇葩情况,针对不同情况,就需要不同的算法。

假如问题有n个阶段,每个阶段都有多个状态,不同阶段的状态数量可以不同,一个阶段的状态可以得到下个阶段所有状态中的几个。要计算出最终阶段的状态数,自然要经历之前每个阶段的某些状态。

好消息是,有时候我们并不需要真的计算所有状态,比如这样一个简单的棋盘问题:从棋盘的左上角到达右下角最短需要几步。答案很明显,提出这个问题,是为了帮助我们理解阶段和状态。某个阶段确实可以有多个状态,正如这个问题中走n步可以走到很多位置一样。但是同样n步中,有哪些位置可以让我们在第n+1中走的最远呢?没错,正是第n步中走的最远的位置。简单点就是:下一步最优是从当前最优得到的。所以为了计算最终的最优解,只需要存储每一步的最优解即可,解决符合这种性质的问题算法叫做“贪心”.如果只看最优状态之间的计算过程,是不是和斐波那契数列的计算过程很像,所以计算的方法叫“递推”。

既然问题都是可以划分成阶段和状态。我们一下子解决了一大类问题:一个阶段的最优可以由前一个阶段的最优得到。

如果一个阶段的最优无法用前一个阶段的最优得到呢?

再来一个迷宫的例子。在计算从起点到终点的最短路线时,你不能只保存当前阶段的状态,因为题目要求你最短,所以必须知道之前走过的所有位置。因为即便当前在的位置不变,之前的路线不同也会影响你之后走的路线。这时你需要保存的是之前每个阶段所经历的那个状态,根据这些信息才能计算出下一个状态。

每个阶段的状态或许不多,但是每个状态都可以转移到下一阶段的多个状态,所以解的复杂度是指数的,因此时间复杂度也是指数。插播一句,之前的路线会影响到下一步的选择,这种情况称之为“后效性”。

迷宫的例子,解决方法太暴力,要避免如何的暴力,契机就在于后效性。

有一类问题,看似需要之前的所有状态,其实不用,使用最长上升子序列的例子来说明为什么不需要暴力搜索,进而引出动态规划的思路。

先来看一个动态规划的教学必备提:

给定一个数列,长度为N,求这个数列的最长上升(递增)子数列(LIS)的长度.以    1 7 2 8 3 4为例。这个数列的最长递增子数列是 1 2 3 4,长度为4;次长的长度为3, 包括 1 7 8; 1 2 3 等.

假装我们年幼无知,想用搜索去寻找最长上升子序列。怎么搜索呢?需要从头到尾依次枚举是否选择当前的数字,每选定一个数字就要去看看是不是满足“上升”的性质,这里第i个阶段就是思考是否要选择第i个数,第i个阶段有两个状态,分别是选和不选。依稀出现了刚刚迷宫找路的影子。等等,容我想想,每当我决定要选择当前数字的时候,只需要和之前选定的一个数字比较就行了。这是和之前迷宫问题本质的不同,可以纵容我们不需要记录之前的所有状态啊。既然我们的选择已经不受之前状态组合的影响,那时间复杂度自然不是指数了,虽然我们不在乎某序列之前都是什么元素,但还是需要知道这个序列的长度。所以只需要记录以某个元素结尾的LIS长度就好。因此第i个阶段的最优解只是由当前第i-1个阶段的最优解得到,然后就得到了DP方程

LIS(i) = max{LIS(j) + 1}  j < i and a[j] < a[i]

所以一个问题,该用递推、贪心、搜索还是动态规划,完全是由这个问题本身阶段间状态的转移方式决定的。

1. 每个阶段只有一个状态——递推。2. 每个阶段的最优状态都是由上一个阶段的最优状态得到——贪心。3. 每个阶段的最优状态是由之前所有阶段的状态组合得到——搜索4. 每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到,而不管之前这个状态是如何得到——动态规划。

每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到,这个性质叫“最优子结构”。而不管之前这个状态是如何得到,这个性质叫“无后效性”。

另:其实动态规划中的最优状态的说法容易产生误导,以为只需要计算最优状态就好,LIS问题确实如何,转移时只用到了每个阶段“选”的状态。但实际上,有的问题往往需要对每个阶段的所有状态都算出一个最优值,然后根据这些最优值再来找最优状态。然后根据这些最优值再来找最优状态。比如背包问题,就需要对前i个包(阶段)容量为j时(状态)计算出最大价值,然后在最后一个阶段中的所有状态中找到最优值。

状态转移方程:f(n,m)=max{f(n-1,m), f(n-1,m-w[n])+P(n,m)}

原文链接: javacodegeeks
译文链接: http://www.importnew.com/13072.html

0 0