本文主要内容摘选自《算法导论》动态规划一章。代码采用书中伪代码形式。
一、动态规划概述
动态规划常常与分治策略、贪心算法同时提及,三种算法都是通过组合子问题的解来求解原问题。在解决某些问题时,其子问题有大量的重叠情况,此时单纯使用分治策略会发现随着输入数据量的增大,运行时间呈指数级增长。动态规划是一种典型的用空间换时间的权衡策略。其核心思想就是将那些重复的子问题的解,记录下来,当需要再次相同子问题时,查表获取结果即可。动态规划通常用来求解最优化问题,适用问题通常有以下两个特点:
1.具有最优子结构性质:问题的最优解由相关子问题的最优解组合而成。
2.有大量的重叠子问题。
二、钢条切割问题
问题:Serling公司购买长钢条,将其切割为短钢条出售。假设切割工序没有成本,不同长度的钢条的售价如下:
那么钢条切割问题就是:给定一段长度为
问题分析:考虑
1.切割为四段,长度为:1,1,1,1;总共卖4*1=4元。
2.切割为三段,长度为:1,1,2;总共卖2*1+1*5=7元。
3.切割为两段,长度为:1,3;总共卖1*1+1*8=9元。
4.切割为两段,长度为:2,2;总共卖2*5=10元。
5.不切割,长度为:4;总共卖1*9=9元。
长度为
三、自顶向下递归实现:
infinity = 1e+16 #无穷大
CUT-ROD(p, n)
if n == 0
return 0
q = -infinity
for i = 1 to n
q = max(q, p[i] + CUT-ROD(p, n-i))
return q
PS:伪代码的好处在于不局限于具体实现语言,聚焦算法思路。
首先,如果输入
但是上述代码,在
当该函数计算
四、动态规划算法一:带备忘录的自顶向下法
infinity = 1e+16 #无穷大
MEMOIZED-CUT-ROD(p, n)
let r[0..n] be a new array
for i = 0 to n
r[i] = -infinity
return MEMOIZED-CUT-ROD-AUX(p, n, r)
MEMOIZED-CUT-ROD-AUX(p, n, r)
if r[n] >= 0
return r[n]
if n == 0
q = 0
else
q = -infinity
for i = 1 to n
q = max(q, p[i] + MEMOIZED-CUT-ROD-AUX(p, n-i, r))
r[n] = q
return q
上述代码与分治不同的地方在于初始化了数组
自顶向下的动态规划算法,仍然不是最理想的。例如在计算
五、动态规划算法二:自底而上法
infinity = 1e+16 #无穷大
BOTTOM-UP-CUT-ROD(p, n)
let r[0..n] be a new array
r[0] = 0
for j = 1 to n
q = -infinity
for i = 1 to j
q = max(q, p[i] + r[j-i])
r[j] = q
return r[n]
自底向上法不再使用函数递归调用,而采用子问题的自然顺序。在切割时,先由最小的1开始切割,若
上述代码中,仍然先初始化一个数组
自底向上算法的时间复杂度也为
六、重构解
在自底向上的解法中,最后的结果只返回了最大收益值,并没有输出具体的切割方案。如果需要得到相应的最有切割方案,在切割计算收益的同时,需要将每次的切割操作记录下来。
infinity = 1e+16 #无穷大
EXTENDED-BOTTOM-UP-CUT-ROD(p, n)
let r[0..n] and s[0..n] be a new array
r[0] = 0
for j = 1 to n
q = -infinity
for i = 1 to j
if q < p[i] + r[j-i]
q = p[i] + r[j-i]
s[j] = i
r[j] = q
return r and s
切割方案打印函数:
PRINT-CUT-ROD-SOLUTION(p, n)
(r, s) = EXTENDED-BOTTOM-UP-CUT-ROD(p, n)
while n > 0
print s[n]
n = n - s[n]
相应的,可以得到如下表:
在EXTENDED-BOTTOM-UP-CUT-ROD函数中,除了数组
小结:
在钢条切割问题中,可以看到,该问题满足两个特点:
1.具有最优子结构性质:一整段钢条切割成两段之后变成两小段,一整段钢条段最优切割方案,由所有两小段的组合方案中最优切割方案构成。
2.无论多长的钢条
动态规划思想的本质,就是找到大量重复计算的子问题,将其解记录下来,当再次遇到的时候通过查表得到解,用少量空间节省大量时间。