一步一步看清动态规划----背包问题(java解)

版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/sinat_34022298/article/details/77653693

———————————更新——————————–

动态规划有两个很重要的概念,无后效性状态,这里参考两篇文章,讲的不错,尤其是第二篇:
什么是无后效性?
动态规划

———————————原文——————————–

题目背景

话说有一哥们去森林里玩发现了一堆宝石,他数了数,一共有n个。 但他身上能装宝石的就只有一个背包,背包的容量为C。这哥们把n个宝石排成一排并编上号: 0,1,2,…,n-1。第i个宝石对应的体积和价值分别为V[i]和W[i] 。排好后这哥们开始思考: 背包总共也就只能装下体积为C的东西,那我要装下哪些宝石才能让我获得最大的利益呢?

OK,如果是你,你会怎么做?你斩钉截铁的说:动态规划啊!恭喜你,答对了。 那么让我们来看看,动态规划中最最最重要的两个概念: 状态和状态转移方程在这个问题中分别是什么。

我们要怎样去定义状态呢?这个状态总不能是凭空想象或是从天上掉下来的吧。 为了方便说明,让我们先实例化上面的问题。一般遇到n,你就果断地给n赋予一个很小的数, 比如n=3。然后设背包容量C=10,三个宝石的体积为5,4,3,对应的价值为20,10,12。 大家都知道正解应该是把体积为5和3的宝石装到背包里, 此时对应的价值是20+12=32。接下来,我们把第三个宝石拿走, 同时背包容量减去第三个宝石的体积(因为它是装入背包的宝石之一), 于是问题的各参数变为:n=2,C=7,体积{5,4},价值{20,10}。好了, 现在这个问题的解是什么,肯定是:把体积为5的宝石放入背包 (然后剩下体积2,装不下第二个宝石,只能眼睁睁看着它溜走),此时价值为20。 这样一来,我们发现,n=3时,放入背包的是0号和2号宝石;当n=2时, 我们放入的是0号宝石。这并不是一个偶然,没错, 这就是传说中的“全局最优解包含局部最优解”(n=2是n=3情况的一个局部子问题)。 绕了那么大的圈子,你可能要问,这都哪跟哪啊?说好的状态呢?说好的状态转移方程呢? 别急,它们已经呼之欲出了。

我们再把上面的例子理一下。当n=2时,我们要求的是前2个宝石, 装到体积为7的背包里能达到的最大价值;当n=3时,我们要求的是前3个宝石, 装到体积为10的背包里能达到的最大价值。有没有发现它们其实是一个句式!OK, 让我们形式化地表示一下它们, 定义d(i,j)为前i个宝石装到剩余体积为j的背包里能达到的最大价值。 那么上面两句话即为:d(2, 7)和d(3, 10)。这样看着真是爽多了, 而这两个看着很爽的符号就是我们要找的状态了。 即状态d(i,j)表示前i个宝石装到剩余体积为j的背包里能达到的最大价值。 上面那么多的文字,用一句话概括就是:根据子问题定义状态!你找到子问题, 状态也就浮出水面了。而我们最终要求解的最大价值即为d(n, C):前n个宝石 (0,1,2…,n-1)装入剩余容量为C的背包中的最大价值。状态好不容易找到了, 状态转移方程呢?顾名思义,状态转移方程就是描述状态是怎么转移的方程。 那么回到例子,d(2, 7)和d(3, 10)是怎么转移的?来,我们来说说2号宝石 (记住宝石编号是从0开始的)。从d(2, 7)到d(3, 10)就隔了这个2号宝石。 它有两种情况,装或者不装入背包。如果装入,在面对前2个宝石时, 背包就只剩下体积7来装它们,而相应的要加上2号宝石的价值12, d(3, 10)=d(2, 10-3)+12=d(2, 7)+12;如果不装入,体积仍为10,价值自然不变了, d(3, 10)=d(2, 10)。记住,d(3, 10)表示的是前3个宝石装入到剩余体积为10 的背包里能达到的最大价值,既然是最大价值,就有d(3, 10)=max{ d(2, 10), d(2, 7)+12 }。好了,这条方程描述了状态d(i, j)的一些关系, 没错,它就是状态转移方程了。把它形式化一下:d(i, j)=max{ d(i-1, j), d(i-1,j-V[i-1]) + W[i-1] }。注意讨论前i个宝石装入背包的时候, 其实是在考查第i-1个宝石装不装入背包(因为宝石是从0开始编号的)。至此, 状态和状态转移方程都已经有了。

程序及详细讲解


public class knapspace{
    public static void main(String[] args) {
        int N = 3 ;     // 宝石个数
        int C = 10 ;    // 书包容量
        int V[] = {0,5,3,4} ;       // 每个宝石的体积,这里前面的0是为了后面表示方便,即V[1]表示为第一个宝石的体积,下同
        int W[] = {0,20,10,12} ;    // 每个宝石的价值
        int d[][] = new int [N+1][C+1] ;

        d[0][C] = 0 ;

        for (int i = 1; i <= N; i++) {
                for (int j= 1; j<=C; j++) {
                    if ( i > 0 && j>=V[i]) {
                    // 状态转移方程
                        d[i][j] = (d[i-1][j] > d[i-1][j-V[i]] + W[i]) ? d[i-1][j] : d[i-1][j-V[i]] + W[i] ;
                    }
                }
            }
            System.out.println(d[N][C]) ;   
    }
}

为了说明情况 ,我们改下程序,让每次执行都打印 d[i][j]看一看,每次都是怎么变化的。

public class knapspace{
    public static void main(String[] args) {
        int N = 3 ;     // 宝石个数
        int C = 10 ;    // 书包容量
        int V[] = {0,5,3,4} ;       // 每个宝石的体积,这里前面的0是为了后面表示方便,即V[1]表示为第一个宝石的体积,下同
        int W[] = {0,20,10,12} ;    // 每个宝石的价值
        int d[][] = new int [N+1][C+1] ;    // 状态数组

        d[0][C] = 0 ;

        for (int i = 1; i <= N; i++) {
            System.out.println("*********  i="+ i +"  *********" ) ;
                for (int j= 1; j<=C; j++) {
                    if ( i > 0 && j>=V[i]) {
                        // 状态转移方程
                        d[i][j] = (d[i-1][j] > d[i-1][j-V[i]] + W[i]) ? d[i-1][j] : d[i-1][j-V[i]] + W[i] ;

                        System.out.println("\t---------- j="+j+"  d[i-1][j]="+d[i-1][j]+"   d[i-1][j-V[i]]+ W[i]="+(d[i-1][j-V[i]]+ W[i])+"   ---------") ;
                        // 打印每次保存状态的数组
                        print_array(d) ;
                    }
                }
                System.out.println() ;
            }
    }
    // 打印保存数据的二位数组
    public static void print_array(int A[][]){
        System.out.println("=======================================================================") ;
        for (int i= 1;i<A.length ;i++ ) {
            for (int j = 1; j<A[0].length; j++) {
                System.out.print(A[i][j] + "\t") ;
            }
            System.out.println();
        }
        System.out.println("=======================================================================") ;
    }
}

我们来看打印出来的 i=1 时候的数据,表示只有第一个宝石的情况:
这里写图片描述

一共三行,表示放三次宝石;10列,表示背包的总体积大小。从程序中可以看到,当求解当前(i=1)最优解的时候,是将当前解的子结构与当前情况做比较(d[i-1][j] 、d[i-1][j-V[i]] + W[i])

d[i-1][j] 表示在体积为j,不装这块宝石的时候,获得的最大价值;此价值通过 i-1 的状态获得,即最优子结构。
d[i-1][j-V[i]] + W[i]: 表示体积在可以装下第 i 块宝石的时候,装下这块宝石的总价值。(d[i-1][j-V[i]]表示在装下当前宝石的时候,剩余体积的最优子解)

可以看到这里进行了5次比较,从可以装下当前宝石的体积 j=5 开始。

下面看一下 i=2 的情况:
这里写图片描述
我们可以看到,i=2的时候,从 j=3 开始,因为第二块宝石的体积大小为3,如果不放入第一块,只放入第二块的话, j=3是符合要求的。
第一行就是 i=1 的求解结果,这里作为 i=2 的最优子结构。

当体积 j<5时,只装入宝石2(V[2] = 3)获取的价值最大;
而在 j>5 的时候,将 d[i-1][j] 、d[i-1][j-V[i]] + W[i] 进行比较,即不装当前宝石与装入当前宝石的总价值比较,选出一个最好的结果。
其中 j>=8时,d[i-1][j] = 20(表示不装当前宝石的最高价值,因为此时二者只可装下一个,所以选总价值最高的装),d[i-1][j-V[i]] + W[i]=30(表示装下当前宝石的最高价值,因为此时二者均可装下,所以总价值为两个宝石价值和),此时最优解为30。

i=3 的情况:
这里写图片描述
这个,按照上面的逻辑同样可以分析清楚。

总结

对于初学者,动态规划是比较难理解的,可以选择多做几题,多思考每一步是怎么达到的。动态规划的思想就是利用 空间换时间,开辟独立的空间存储最优子解,求新问题解=子问题+状态转移,这样可以避免掉对于子问题的重复计算。在动态规划中,状态和状态转移方程式很重要的。
以上个人观点,欢迎讨论。

参考

动态规划之背包问题
Dynamic Programming - From Novice to Advanced
什么是无后效性?
动态规划

展开阅读全文

没有更多推荐了,返回首页