动态规划

本文主要参考自《算法导论》

动态规划

动态规划方法通常用来求解最优化问题。这类问题可以有很多可行解,每个解都有一个值,我们希望寻找具有最优值的解。我们称这样的解为问题的一个最优解,而不是最优解,因为可能有多个解都达到最优值。

需要注意的是,动态规划是求解某类问题的一种方法,是考察问题的一种途径,而不是一种特殊的算法。

动态规划的求解步骤如下:

1. 刻画一个最优解的结构特征
2. 递归地定义最优解的值
3. 计算最优解的值,通常采用自底向上的方法
4. 利用计算出的信息构造一个最优解。

动态规划运用要求

  • 最优子结构:一个问题的最优解包含其子问题的最优解。
  • 重叠子问题:问题的递归算法会反复地求解相同的子问题。

下面来看个最简单的例子:

裴波那契数列:

1,1,2,3,5,8……
从第三项开始,每一项等于前两项之和,现在要求该数列的第 n 项。(虽然这个求的不是最优解,但解题思路与动态规划相似,可以从此入门)

我们可以很快得到下面这样的递归示意图, F i b ( n ) = F i b ( n − 1 ) + F i b ( n − 2 ) Fib(n)=Fib(n-1)+Fib(n-2) Fib(n)=Fib(n1)+Fib(n2)

KIXI3T.png

由此可以写出下面的代码:

long Fibonacci(int n) {
    if (n == 0)
        return 0;
    else if (n == 1)
        return 1;
    else
        return Fibonacci(n - 1) + Fibonacci(n-2);
}

这是最简单粗暴的方法,最大的问题就在于会进行大量的重复计算,看示意图我们就可以看到,Fib(3) 和 Fib(2) 就重复计算了两次,如果 n 再大一些,那么要重复计算的数据就将呈指数增长,这是我们不愿看到的。

下面这个解法就避开了重复计算的问题,其对应的动态规划解法是自底向上法(下面的数组 arr 对应着动态规划中的 DP 数组,放在这里只是为了跟动态规划对接一下,DP 数组存的是各个阶段的状态)。

int* Fibonacci(int n) {
	int arr[n] = {1,1};
    if (n <= 2)
        return 1;
    else {
        long num1 = 1;
        long num2 = 1;
        for (int i = 2;i < n - 1;i++) {
            num2 = num1 + num2;
            num1 = num2 - num1;
        	arr[i] = num2;
        }
        arr[i] = num2 + num1;
        return arr;
    }
}

arr 从第一项开始,存储了到第 n 项的每个裴波那契数列的值,这是动态规划解决方法中的自底向上的解法,其中 arr 对应着动态规划中的 DP 数组,用来存储各个阶段的最优解,关于 DP 数组,看到下面的题目会有一个更直观的认识。

钢条切割

假设有一家出售钢条的公司,其中钢条的价格表如下:

KopVc6.md.png

现有一个长度为 n 英寸的钢条,该如何切割使得销售收益最大?

问题分析

如果一个最优解将钢条切割为 k 段,那么最优切割方案 n = i 1 + i 2 + . . . + i k n=i_1+i_2+...+i_k n=i1+i2+...+ik,得到的最大收益为:
r n = p i 1 + p i 2 + . . . + p i k r_n=p_{i1}+p_{i2}+...+p_{ik} rn=pi1+pi2+...+pik

对于 n ≥ 1 n≥1 n1 r n = m a x ( p n , r 1 + r n − 1 , r 2 + r n − 2 , r 3 + r n − 3 , . . . , r n − 1 + r 1 ) r_n=max(p_n,r_1+r_{n-1},r_2+r_{n-2},r_3+r_{n-3},...,r_{n-1}+r_{1}) rn=max(pn,r1+rn1,r2+rn2,r3+rn3,...,rn1+r1),其中 p n p_n pn 对应不切割,对于每个 i = 1 , 2 , . . . , n − 1 i=1,2,...,n-1 i=1,2,...,n1,首先将钢条切割为长度为 i i i n − i n-i ni 的两段,接着求解这两段的最优切割收益 r i r_i ri r n − i r_{n-i} rni (每种方案的最优收益为两段的最优收益之和)。 对该式子进行整理可得:
KoiS2R.png
由此可得如下的伪代码:

CUT-ROD( p, n )
    if  n == 0
        return 0
    q= -for  i = 1 to n
        q = max(q, p[i]+CUT-ROD( p , n-i ))        
    return q

其中 p 为价格数组,n 为钢管长度,q 即为 r n r_n rn

以上便为该题的最为直观的也是计算量十分巨大的解法。

我们看一下该解法的递归示意图,每个节点代表的是钢条的剩余长度:
Ko0ET0.md.png

我们可以看到,就像第一个例子裴波那契数列一样,它对 2,3,1 节点进行了大量的重复计算,属于重叠子问题,而且根据分析我们也可以知道,要求他的最大收益,就要求他的子问题的最大收益,它属于最优子结构,因此可以使用动态规划。

  1. 带备忘的自顶向下法:此方法仍按自然的递归形式编写过程,但过程会保存每个子问题的解,通常保存在数组中,当需要一个子问题的解时,过程首先检查是否已经保存过此解,如果是,则直接返回保存的值,否则,按通常方式计算这个子问题。

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

对应的伪代码:
方法1:

MEMOIZED-CUT-ROD( p, n )
    if  r[n] >= 0              //判断长度为 n 的钢条的最大收益 r[n] 是否已经算出,其中没算出的都为负数
        return r[n]
    if  n == 0
        q=0
    else q= -for  i=1 to n
            q=max ( q, p[i]+MEMOIZED-CUT-ROD( p , n-i ) ) 

    r[n]=q       
    return q

方法2:

BOTTOM-UP-CUT-ROD( p, n )
    let r[0..n] be a new array
    r[0]=0
    for  j=1 to n               //计算钢条长度从 0 到 n 的所有最优解
        q= -for i=1 to j                     //对应递归式,其中前提子问题已经解出存放在数组 r 中
            q=max ( q, p[i]+r[j-i])       
        r[j]=q    
    return r[n]

其中 r 即为动态规划的 DP 数组,存着每个阶段的状态,每一个最优解就是从 DP 数组中找到前提子问题的状态,再进行运算比较获得的。

总结

解决动态规划问题,首先要判断有没有重叠子问题,这一步很容易跟分治法搞混;接着便是分析问题,总结出该问题的递归方程;有了递归方程后,就可以考虑使用两种方法中的任一种来结题了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值