动态规划算法分析及实例——求解完全背包问题(java实现)

来源:互联网 发布:java中xml文件的作用 编辑:程序博客网 时间:2024/05/29 14:40


一、动态规划算法简介

 

       动态规划算法和分治法类似,都是把子问题的解组合从而求解原问题。但分治法是将一个问题划分为互不相交的子问题,递归地求解子问题,再将他们的解组合起来,求出原问题的解。而动态规划算法针对的是子问题重叠的情况,即不同的子问题中还有公共的子问题,也就是说子问题的求解是递归进行的,还可以将子问题划分为更小的子问题。在这种情况下,分治法就会做很多不必要的工作,去反复地求解那些公共子问题。而动态规划算法对所有的子问题只求解一次,并且将每一次所得的解保存在一个表格中,这就避免了很多不必要的计算。

       动态规划算法通常用来求解最优化问题。这类问题可能有很多可行的解,每个解都有一个值,而我们要找的就是其中具有最优值的解,这样的解就被称为问题的一个最优解,而不是全局的最优解,因为可能有多个解都有最优值。


二、动态规划算法的设计思想

 

         为了理解动态规划算法的基本思想,我们先来看一个简单的例子。本例取自屈婉玲《算法分析与设计(第二版)》(清华大学出版社)。


    (一)多起点、多终点的最短路径问题

 

          在实际问题中,我们经常会遇到路径选择的问题。如图所示,有5个起点S1,…,S5,5个终点T1,…,T5,其余结点是途经结点。各结点之间用边连接,边上的整数表示长度。从起点Si可以通过4条从左到右的边SiAjBkClTm到达终点Tm,这就是一条从起点Si到终点Tm的路径,路径的长度就是这4条边的长度之和。我们的问题是:给定道路图,在所有起点到终点的路径中找一条长度最短的路径。




实际上这个问题用穷举法就可以解决,我们可以列举出每一个起点到每一个终点的所有可能路径,然后从中选取最短的一条路径。但是结点数目比较多的时候,时间上的消耗可能难以承受。

       我们可以尝试用动态规划算法来解决这个问题。从终点向起点回推,把求解过程分成4步,每一步对应的子问题的终点不变,但起点逐步前移,使得前一步已经求解的问题恰好是后面新问题的子问题,到最后一步求解的最大的子问题即为原始问题。具体来说,所有子问题的终点都是Tm,但起点不同。第一步子问题的起点是Cl,第二步子问题的起点是Bk,第三步子问题的起点是Aj,第四步子问题的起点是Si,求解到第四步的时候,其实已经是原始问题了。那么每一步需要求解的就是:从当前起点到终点的最短路径和长度。

       第一步要确定从Cl到终点的最短路径。假设我们取的是C1,它到终点有两条路,向上走到T1或向下走到T2,显然最短路径为2,把这个结果标在C1的上方,记作“u,2”,其中u表示到终点的最短路径应该向上,2表示这条最短路径的长度。接下来看C2,和判断C1相似,C2到终点的最短路径长度为3,并且方向向下,所以标记为“d,3”,同样地,C3C4也是这样判断。当C3C4判断完成时,第一步的判断就全部完成了。

       第二步要确定Bk到终点的最短路径。我们可以先求出BkCl的最短路径,再加上前一步求出的Cl到终点的最短路径,自然就是Bk到终点的最短路径。先看B1,从B1只能向下到C1,接着从C1走最短路径到终点,于是B1到终点的最短路径长度为9 + 2 = 11,把这个结果标记在B1的上方,记作“d,11”。接下来考虑B2,与B1不同,从B2既可以向上走到C1,从C1走最短路径到终点,也可以向下走到C2,从C2走最短路径到终点。这两种走法哪种的总路径长度最短,我们就应该选择哪种。显然,从B2到终点的最短路径长度应为3 + 2 = 5(B2C1T1),在B2上方标记“u,5”。接着判断B3B4B5,到此为止第二步的判断全部完成。

       类似地可以AjSi到终点的最短路径,当Si判断完毕时,整个问题就已经求解完毕了。下图是标记后的结果:




       动态规划算法的好处就是:在判断时只考虑由前面子问题的最优解可能的延伸结果,从而把许多不可能成为最优解的部分都从搜索中去掉了,因此能够提高效率。


     (二)使用动态规划技术的必要条件


      我们再来总结一下上面的例子,它的主要特征是:求解时需要分为多个阶段;求解过程是多不判断,从小到大地依次求解每个子问题,最后求解的子问题就是原始问题;子问题目标函数的最小值之间存在依赖关系,每一次都将子问题的解记录下来,以备后面求解时使用。其中子问题目标函数的最小值之间的依赖关系是最关键的,这被称为优化原则

       正是由于优化原则,所以在考虑后面较大子问题的最优解时,只需考虑它的子问题的最优解所延伸的结果。需要注意的是,实际上并不是所有的组合优化问题都适用于优化原则,有时某一个子问题的解不一定是局部最优解,但是当把它延伸成整个问题的解时反而成了最优解。如果对这样的问题使用动态规划算法,就有可能出错。


、动态规划算法的典型应用


       背包问题

       背包问题是一个著名的NP难问题。问题如下:一个旅行者准备随身携带一个背包。可以放入背包的物品有n种,物品j的重量和价值分别为wjvjj = 1,2,…,n。如果背包的最大重量限制是b,怎样选择放入背包的物品以使得背包的价值最大?

       这是一个组合优化问题,设xj表示装入背包的第j种物品的数量,那么目标函数和约束条件是:

                                                                              

       如果组合优化问题的目标函数和约束条件都是线性函数,称为线性规划问题,如果线性规划问题的变量xj都是非负整数,则称为整数规划问题。背包问题就是整数规划问题。限制所有的xj = 0或1时的背包问题称为0-1背包问题。需要注意的是,我们今天讨论的是完全背包问题,即不对放入背包的物品数量作限制

       很显然背包问题满足上述的优化原则,因为我们可以物品的种类和背包重量的限制把这个问题分成多个阶段求解Fk(y)表示只允许装前k种物品,并且背包总重量不超过y时背包的最大价值,分两种情况考虑:不装第k种物品或者至少装1件第k种物品。如果不装第k种物品,那么只能用前k - 1种物品装入背包,此时的背包最大限重仍然是y,在这种情况下,背包的最大价值就是Fk -1(y)。如果装了一件第k种物品,此时背包的价值是vk,重量是wk,接下来还得从前k种物品中选择一种装入背包,因为这个问题对物品的装入数量不作限制。于是这个问题就变成了在背包限重为y -wk的情况下如何用前k种物品装入背包使背包达到最大价值Fk(y- wk),也就是说我们需要从Fk -1(y)与Fk(y -wk) +vk这两种情况种选取价值比较大的作为最优选择。用数学函数表示就是:Fk(y) = max{Fk -1(y),Fk(y -wk) +vk}。

       我们再来说明几个重要的问题:F0(y)是背包不装物品的价值,显然等于0;Fk(0)是背包重量限制为0时的最大价值,当然也等于0;F1(y)是只能用第一种物品装入背包,并且此时的背包限重为y,为了保证背包不超重,第一种物品至多能装y /w1显然y /w1为整数,因此此时背包的价值为(y / w1) ×v1。还有一种情况,因为在递推式中有Fk(y -wk),在递推的过程中,某些y -wk可能得到负值,这意味着背包此刻能够承受的重量已经小于第k种物品的重量,所以这种装法需要排除掉。

        我们来考虑下面的例子:

                                                                v1 = 4,v2 = 2,v3 = 1,v4 = 10,v5 = 2

                                                                w1 = 12,w2 = 2,w3= 1,w4 = 4,w5 = 1

                                                                b = 15

      Fk(y)的计算表如下图所示,其中省略了所有Fk(0)的值




      上面的优化函数表只是计算了各个子问题的局部最优解,表中最后一项F5(15) = 36,这就是背包的最大价值。那怎样选择物品才可以得到这个最大价值呢?我们可以这样做:设立一个标记,ik(y)表示子问题到达局部最优解Fk(y)时所用到物品的最大标号,ik(y)称为标记函数在计算Fk(y)时,如果Fk - 1(y)比Fk(y - wk) +vk大,这就说明当背包装入前k - 1种物品时得到了最大价值,这时装入背包物品的最大标号就应该和前k - 1种物品的最大标号一致,否则就是选择标号为k的物品使背包达到了最大价值。不难看出,ik(y)是不需要额外计算的,只要对照优化函数表,将相关子问题获得最大价值时的k值填入标记函数表即可。上面问题的标记函数表如下:




      根据标记函数表,我们可以追踪问题的求解过程。比如说由i5(15) = 5可知5号物品至少用了一个,占用背包的重量为1,背包剩余重量为14,继续检查i5(14) = 5,占用背包的重量为1,背包剩余重量为13,再检查i5(13) = 5,占用背包的重量为1,背包剩余重量为12,检查i5(12) = 4,占用背包的重量为4,背包剩余重量为8……像这样一直检查下去,直到剩余背包的重量小于等于零,说明不能再装任何物品了,检查完毕。

       我们可以把这个检查的过程用伪代码来描述:

      

       下面将给出完全背包问题的完整求解过程,用java语言描述:

// 物品类public class Goods {        public int label;  // 物品的标签    private int weight;  // 物品的重量    private int value;  // 物品的价值    public int sum = 0;  // 装入该物品的数量        public Goods(int label, int weight, int value) {        this.label = label;          this.weight = weight;        this.value = value;    }        public int getWeight() {        return weight;    }        public int getValue() {        return value;    }        public int getSum() {        return sum;    }}

// 背包问题类import static java.lang.Integer.max;public class PackageProblem {        // 优化函数    public static void majorisedFunction() {        // 初始化5个物品的重量及价值        Goods[] goods = { new Goods(0, 0, 0),  // 加入此物品是为了后面方便循环变量从1开始访问                          new Goods(1, 12, 4),                          new Goods(2, 2, 2),                          new Goods(3, 1, 1),                          new Goods(4, 4, 10),                          new Goods(5, 1, 2) };        int upboundweight = 15;  // 背包的重量上限是15        int[][] addgoods = new int[goods.length][upboundweight + 1];  // 数组的第二维加1是为了后面方便循环变量从1开始访问        for(int i = 0; i < goods.length; i++) {  // 初始化表格            for(int j = 0; j < upboundweight + 1; j++) {                addgoods[i][j] = 0;            }        }        int h, k, max;        for(h = 1; h <= goods.length - 1; h++) {            for(k = 1; k <= upboundweight; k++) {                if(k >= goods[h].getWeight()) {  // 如果当前背包的承重大于等于物品                    if(addgoods[h - 1][k] > addgoods[h][k - goods[h].getWeight()] + goods[h].getValue()) {                        max = addgoods[h - 1][k];                    }                    else                         max = addgoods[h][k - goods[h].getWeight()] + goods[h].getValue();                    addgoods[h][k] = max;                }                else addgoods[h][k] = addgoods[h - 1][k];            }        }        System.out.println("背包达到的最大价值为:" + addgoods[goods.length - 1][upboundweight]);        // 输出优化函数表        System.out.println("优化函数表:");        for(int i = 1; i <= goods.length - 1; i++) {            System.out.print("k = " + i + " ");            for(int j = 1; j <= upboundweight; j++) {                System.out.printf("%4d", addgoods[i][j]);            }            System.out.println();        }         }        // 标记函数    public static void markingFunction() {        // 输入标记函数表        int[][] marking = {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1},                           {0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2},                           {3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3},                           {3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4},                           {5, 5, 5, 4, 5, 5, 5, 4, 5, 5, 5, 4, 5, 5, 5}};        Goods[] goods = { new Goods(1, 12, 4),                          new Goods(2, 2, 2),                          new Goods(3, 1, 1),                          new Goods(4, 4, 10),                          new Goods(5, 1, 2) };        for(int i = 1; i <= 5; i++) {            for(int j = 0; j < 5; j++) {                goods[j].sum = 0;  // 初始化每种物品装入的数量            }            int y = 15;            for(int j = i - 1; j >= 0; j--) {  // 遍历i种物品                j = marking[j][y - 1] - 1;                goods[j].sum = 1;                y -= goods[j].getWeight();                while(y > 0 && marking[j][y - 1] - 1 == j) {  // 这一步必须保证y > 0                    if(y - goods[j].getWeight() < 0) {  // 背包剩余重量小于0时跳出循环                        break;                    }                    else {                        y -= goods[j].getWeight();                        goods[j].sum += 1;                    }                }                if(j - 1 >= 0 && y - goods[j - 1].getWeight() <= 0) {  // 这一步判断极其重要,如果剩余的重量不足以再装入任何物品,则跳出for循环                    break;                }            }            System.out.println("k = " + i + " 且达到最大价值时装入物品的结果:  ");            for(int j = 0; j < 5; j++) {                System.out.print("物品" + (j + 1) + ":" + goods[j].sum + "    ");            }             System.out.println();        }    }    }

// 测试类public class Test {       public static void main(String[] args) {        PackageProblem.majorisedFunction();        PackageProblem.markingFunction();    }}

       参考书目:Thomas H.Cormen等——《算法导论(第3版)》(机械工业出版社)

                         屈婉玲等——《算法设计与分析(第2版)》(清华大学出版社)

1 0
原创粉丝点击