动态规划算法——钢条切割问题

动态规划是通过组合子问题的解来求解原问题。与分治方法不同的是,动态规划应用于子问题重叠的情况,即不同的子问题具有公共的子子问题。在这种情况下,分治策略会重复的计算那些公共子问题。而动态规划是对每个子子问题只求解一次,将其保存在一个表格中,从而避免重复计算这些问题。
动态规划通常用于求解最优化问题(optimization problem)。这类问题拥有多个解,我们希望从中计算出最优解。当然,有些问题可能会不止一个最优解,此时,我们只需按照需求或计算一个最优解,或计算出所有的最优解。
通常有4个步骤设计一个动态规划算法。

1. 刻画一个最优解的结构特征;
2. 递归的定义最优解的值;
3. 计算最优解的值,通常有两种方法,自底向上和自顶向下。
4. 利用计算出的信息构造一个最优解。
钢条切割的例子

给定一个长度为n英寸的钢条和一个价格表 pi(i=1,2,...n) ,求切割钢条的方案,使得销售收益 rn 最大。
假设切割长度与价格表如下所示

长度 i 1 2 3 4 5 6 7 8 9 10
价格pi 1589101717202430

因为在钢条左端 i(i=1,2,...n1) 处可以选择切割和不切割两种方式,所以长度为 n 的钢条共有2(n1)中切割方案。当 n=4 时,即有一个长度为4 的钢条,怎样切割才能获得最大收益。可以有8中方案,如下。

  • 4 = 4 , 收益 r=9
  • 4 = 1+3,收益为 r=9
  • 4 = 2+2,收益为 r=10
  • 4 = 3+1 ,收益为 r=9
  • 4 = 1+1+2, 收益为 r=7
  • 4 = 1+2+1, 收益为 r=7
  • 4 = 2+1+1,收益为 r=7
  • 4=1+1+1+1,收益为 r=4

很明显,当4=2+2时,可以得到最高的收益。那么,我们怎样用形式化的语言来描述它呢?

  1. 我们先切割第一段,我们可以在第1个位置切割,即4=1+3,也可以在第2个位置切割,即4=2+2等等。
  2. 就得到1+3,2+2,3+1这些种情况。同时,还要考虑钢条不切割的情况。
  3. 计算最大收益。因为我们的目的是求怎样切割(或者干脆不切割)会得到最大收益。那么,最大收益 r=max(r(4),r(3)+r(1),r(2)+(2),r(1)+r(3)
  4. 依次切割,直到得到最大收益。

假设一个最优解是要将钢条切割成 k 段,那么最优解的切割方案为:

n=i1+i2+i3+...+ik

将钢条切割长度分别为 i1,i2,...ik 长度的钢条,得到的最大收益为:

rn=p(i1)+p(i2)+...p(ik)

更一般的,对于 rn(n>=1) ,我们可以用 更短的钢条的 最优切割来描述它。
rn=max(pn,r1+rn1,r2+rn2,...ri+rni)

第一个参数 pn 为不切割的方案。其他 n1 个参数对应 n1 种方案:对每个 i=1,2,3..n1 ,首先将钢条的分为长度为 i ni的两段钢条,分别求出这两段钢条的最优解 ri rni (每种方案的最优收益为两段的最优收益之和)。
为了求解规模为 n 的原问题,我们先求解形式完全一样,但规模更小的子问题。即当完成首次切割后,我们将两段钢条看成两个独立的钢条切割问题实例。我们通过组合相关子问题的最优解,并在所有可能的两段切割方案中选取收益最大的,构成原问题的最优解。钢条问题满足最优子结构

最优子结构:问题的最优解由相关子问题的最优解组合而成,而这些子问题都可以独立求解。

使用动态规划解决钢条切割的问题

虽然我们可以用较少较容易理解的递归代码解出此问题,然而,递归会重复计算相同的子问题,导致程序的运行时间以指数级的速度增长,即时间复杂度为O(2n)

动态规划的思想:使用递归方法之所以效率这么低,是因为它会重复计算相同的子问题。因此,动态规划方法仔细安排求解顺序,对每个子问题只求解一次,并将其结果保存下来。如果再次需要子问题的解,只需查找保存的结果,而不必重新计算。因此,动态规划方法时典型的时空权衡(time-memory trade-off)的例子。

动态规划有两种等价的实现方法:

带备忘录的自顶向下方法(top-down with memorization)

此方法仍然按照自然的递归形式编写过程,但过程中会保存每个子问题的解(通常是保存在数组或者散列表中),当需要一个子问题的解时,会先判断是否保存过此解。如果是,则直接返回保存的值,从而节省了计算时间。否则,按照常规方式进行计算。

代码如下


public static int memorizedCutRod(int[] p, int n){
        int result = 0;
        //保存已计算过的子问题的解的数组
        int res[] = new int[n+1];
        for (int i = 0; i < res.length; i++) {
            res[i] = -1;
        }
        result = memorizedCutRodAux(p, n, res);
        return result;
    }

    public static int memorizedCutRodAux(int[] p, int n, int[] r){
        //如果已经计算过该子问题的解,直接返回
        if(r[n]>=0){
            return r[n];
        }

        int q = -1;
        if(n==0){
             q = 0;
        } else{
            //r(n)=max(p[i]+r(n-i))
            //p[i]表示切割成长度为i的钢条的收益
            //r(n-i)剩余钢条的最大收益值
             for(int i = 1;i<=n;i++){
                 q = Math.max(q, p[i]+memorizedCutRodAux(p, n-i, r));
             }
        }
        r[n] = q;
        return q;
    }
自底向上的方法(bottom-up method)

这种方法一般需要恰当定义子问题的规模的概念,使得任何子问题的求解都只依赖于更小的子问题的求解。因而,我们可以将子问题按照规模排序,按照有小到大的顺序进行求解。当求解某个子问题时,它所依赖的那些更小的子问题都已经求解完毕,结果已经保存。每个子问题只需要求解一次,当我们求解它时,它的所有前提子问题都已经求解完成。

public static int bottomUpCutRod(int[] p, int n){
        //保存子问题的结果,res[n]就是我们所需的最优解
        int[] res = new int[n+1];
        //依次求出规模为i = 1...n的子问题
        for(int i=1; i<=n; i++){
            int q = -1;
            for(int j=1;j<=i;j++){
                q = Math.max(q, p[j]+res[i-j]);
            }
            res[i] = q;
        }

        return res[n];

    }

现在我们只是求出来收益的最大值,并没有求得解本身,即给出切割后每段钢条的长度。我们可以扩展动态规划算法,使之对每个子问题不仅保存最优收益值,还保存对应的切割方案。

代码如下


public static void extendedBottomUpCutRod(int[] p, int n){
        int[] res = new int[n+1];
        //用来保存最优解的切割的钢条的长度
        int[] solve = new int[n+1];

        res[0] = 0;
        for (int i = 1; i <= n; i++) {
            int q = -1;
            for(int j = 1; j<=i;j++){
                if(q<p[j]+res[i-j]){
                    q = p[j]+res[i-j];
                    solve[i] = j;
                }
            }
            res[i] = q;
        }

        print(res[n], solve, n);
    }

    public static void print(int maxValue, int[] solve, int n) {
        System.out.println(maxValue);
        while(n>0){
            System.out.print(solve[n]+", ");
            n = n - solve[n];
        }
    }

测试代码


public static void main(String[] args) {
        int[] p = {0,1,5,8,9,10,17,17,20,24,30};
        int n = 9;
        int result = memorizedCutRod(p, n);
        System.out.println(result);
        int result2 = bottomUpCutRod(p, n);
        System.out.println(result);

        extendedBottomUpCutRod(p, n);
    }
  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值