动态规划算法-背包问题

608 阅读6分钟

动态规划定义

任何数学递推公式都可以直接转换成递推算法,但是编译器常常不能正确对待递归算法。将递归重新写成非递归算法,让后者把些子问题的答案系统地记录在一个表内。利用这种方法的一种技巧叫做动态规划

注:由已知推未知就是递推,由未知推未知就是递归,这里说的数学递推公式有别与递推算法。具体解释如下: 如果数列{an}的第n项与它前一项或几项的关系可以用一个式子来表示,那么这个公式叫做这个数列的递推公式。

为什么编译器常常不能正确对待递归?

递归4条基本法则

  1. 基准情形。必须有某些基准情形,它无需递归就能解出。
  2. 不断推进。对于那些递归求解的步骤,每一次递归调用都必须要使情况朝一种
  3. 设计法则。假设所有的递归调用都能运行。
  4. 合成效益法则(compound interest rule)。在求解一个问题的实例时,切勿在不同的递归调用中做重复的操作。    递归的4条法则中,效率低下的递归实现经常触犯第4条法则,即合成效益法则,也是编译器通常无法正确对待递归的原因。下面举例说明。

以求斐波那契数为例说明

问题说明

有通项公式 f(n)=f(n-1)+f(n-2); f(0)=f(1)=1;求任意n对应的f(n)

注意:目前有的编译器可以优化尾递归

递归解法及存在的问题

    /**
     * 递归实现违反合成效益法则
     * */
    public static int fib(int n){
        if(n<=1){
            return 1;
        }else{
            return fib(n-1)+fib(n-2);
        }
    }

以求f6为例,计算f6需要计算f5和f4,而算f5是有需要计算f4+f3,则必定有重复计算的部分。具体详细见下图,(下图紫色部分都是多余计算)

image

分析

由于计算F(N)只需要知道F(N-1)和F(N-2),因此我们只需要保留最近算出的两个斐波那契数,并从f(2)开始一直计算的f(n)即可。

代码实现

/**
     * 动态规划版本,保证没有多余的计算,
     * 以last 保存f(i-1)的值,nextToLast保存f(i-2)
     * answer 保存f(i)的值
     * */
    public static int fibonacci(int n){
        if(n<=1){
            return 1;
        }
        int last=1;
        int nextToLast=1;
        int answer=1;
        for(int i=2;i<=n;i++){
            answer=last+nextToLast;
            nextToLast=last;
            last=answer;
        }
        return answer;
    }

小试牛刀解背包问题

问题说明

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

分析

临时背包总价值=Max{选取当前项背包总价值,不选取当前项背包总价值},转换为数学公式为:

选取当前项时, 临时背包总价值=val[item-1]+V[item-1][weight-wt[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]);

进过上步骤分析,我们仅需保留以item为行,以权重weight为列的二维数组即可。具体实现如下:

代码实现(非自实现)

 /**
     * @param val 权重数组
     * @param wt  重量数组
     * @param W   总权重
     * @return    背包中使得背包内物品的总价值最大时的重量
     */
    public static int knapsack(int val[], int wt[], int W) {
        //物品数量总和
        int N = wt.length; 
 
        //创建一个二维数组
        //行最多存储N个物品,列最多为总权重W,下边N+1和W+1是保证从1开始
        int[][] V = new int[N + 1][W + 1]; 
 
        
        //将行为 0或者列为0的值,都设置为0
        for (int col = 0; col <= W; col++) {
            V[0][col] = 0;
        }
        for (int row = 0; row <= N; row++) {
            V[row][0] = 0;
        }
        //从1开始遍历N个物品
        for (int item=1;item<=N;item++){
            //一行一行的填充数据
            for (int weight=1;weight<=W;weight++){
              
                if (wt[item-1]<=weight){
                    //选取(当前项值+之前项去掉当前项权重的值)与不取当前项的值得最大者
                    V[item][weight]=Math.max (val[item-1]+V[item-1][weight-wt[item-1]], V[item-1][weight]);
                }else {//不选取当前项,以之前项代替
                    V[item][weight]=V[item-1][weight];
                }
            }
 
        }
 
        //打印最终矩阵
        for (int[] rows : V) {
            for (int col : rows) {
                System.out.format("%5d", col);
            }
            System.out.println();
        }
        //返回结果
        return V[N][W];
    }

总结

  1. 编译器一般不能很好的处理递归,尤其是违反合成效益法则的递归
  2. 动态规划需要分析,其重点在于以适用的数据结构保持递归步骤中的中间值。
  3. 是否需要将递归转换为非递归需要以实际项目的情况,酌情考虑。

代码地址

github地址

求Fibonacci数

动态规划算法解背包

码云地址

求Fibonacci数    动态规划算法解背包