解释:动态规划(dynamic programming)与分治方法相似,都是通过组合子问题的解来求解原问题。
优点:动态规划比分治方法高明之处在于对每个子子问题只求解一次,将其保存,无需重新计算。
动态规划设计步骤:
- 刻画一个最优解的结构特征。
- 递归地定义最优解的值。
- 计算最优解的值,通常再用自底向上的方法。(维护一些额外信息,以便构造步骤4)
- 利用计算出的信息构造一个最优解。
钢条切割问题
描述:给定一段长度为n英寸的钢条和一个价格表Pi(i = 1,2,...,n),求切割钢条的方案,使得销售收益Rn最大。
价格表:
长度i 1 2 3 4 5 6 7 8 9 10
价格Pi 1 5 8 9 10 17 17 20 24 30
我们可以明显看出来
R1 = 1,切割方案1 = 1(无切割)
R1 = 5,切割方案2 = 2(无切割)
R1 = 8,切割方案3 = 3(无切割)
R1 = 10,切割方案4 = 2 + 2
由此可以总结出来一个公式Rn = max(Pn, R1 + Rn-1, R2 + Rn-2,..., Rn-1 + R1)
由此可以写出自顶向下的递归实现伪代码
CUT-ROD(P,n) //P为价格数组[1..n],n为长度为n的最大收益 if n == 0 //如果n = 0 不会有收益,则返回0 return 0 q = -无穷 //把最大收益初始化为负无穷 for i = 1 to n //循环1到n的切割方式 q = max(q,P[i] + CUT-ROD(P,n-i)) //最大收益等于当前收益或切割成n段的收益中的最大值 return q //返回最大收益
递归的弊病:你会发现一个问题,这个计算非常的慢,在n=40的情况下,就基本得算好几分钟。之所以效率这么差的原因在于,这个函数反复计算相同的参数的递归调用。即反复求解相同的子问题。
动态规划的实质:这时候就需要动态规划的出现了,动态规划会仔细安排求解顺序,对每个子问题只求解一次,并将结果保存下来。随后再次需要此子问题的解,只需要找保存的结果,而不必重新计算。因此归根结底,动态规划是付出额外的内存空间来节省计算时间,是典型的时空权衡。
动态规划的两种实现方法
第一种 带备忘的自顶向下法
第二种 自底向上法
第一种仍然是递归,只不过加了一个数组来存储每个子问题的解。
第二种就是从小问题解决,在逐渐推出大问题,类似于一道题的自然衍生过程。
对于钢条切割的第一种方法伪代码
MEMOIZED-CUT-ROD(P,n) let r[0..n] be a new array //新建新的数组r for i = 0 to n //初始化r r[i] = -无穷 return MEMOIZED-CUT-ROD-AUX(P,n,r) MEMOIZED-CUT-ROD-AUX(P,n,r) if r[n] >= 0 //如果r[n] >= 0说明这个切割算过一次,直接返回结果 return r[n] if n == 0 //如果切割条数为0,则返回0 q = 0 else q = -无穷 //初始化 for i = 1 to n q = max(q, P[i] + MEMOIZED-CUT-ROD-AUX(P, n-i, r)) //最优切割价格为当前价格或切割数为n-i的价格最大值 r[n] = q //将最优切割值记录 return q
第二种方法的伪代码
BOTTOM-UP-CUT-ROD(P,n) let r[0..n] be a new array //新建r数组 r[0] = 0 //初始化 0切割返回0 for j = 1 to n //循环从1条到n条长的最优解 q = -无穷 for i = 1 to j //循环从1切割到j切割的最优解 q = max(q, P[i] + r[j-i]) //最优解q = 当前值或长度为i的价格和长度为j-i的最优解的和的最优值 r[j] = q //记录最优解 return q