第四部分 高级技术和分析技术
第14章 动态规划
-
动态规划与分治方法相似,都是通过组合子问题的解来求解原问题
分治方法 将问题划分为互不相交的子问题,递归地求解子问题,再将所有解组合起来 动态规划 应用于子问题重叠的情况,即不同子问题具有公共的子子问题 -
思想:求解每种子子问题,将其解保存在一个表格中。
-
应用:通常求解最优化问题
- 步骤:
- 刻画一个最优解的结构特征
- 递归地定义最优解的值
- 计算最优解的值,通常采用自底向上的方法
- 利用计算出的信息构造一个最优解
- 步骤:
14.1 钢条切割
-
钢条切割问题:给定一段长度为n英寸的钢条和一个价格表 p i ( i = 1 , 2 , ⋅ ⋅ ⋅ , n ) p_i(i=1,2,···,n) pi(i=1,2,⋅⋅⋅,n),求切割方案,使得销售收益 r n r_n rn最大。如果长度为n英寸的钢条的价格 p n p_n pn足够大,则不需要切割。
-
价格表样例:
长度 i i i 1 2 3 4 5 6 7 8 9 10 价格 p i p_i pi 1 5 8 9 10 17 17 20 24 30 -
r i ( i = 1 , 2 , ⋅ ⋅ ⋅ , 10 ) r_i(i=1,2,···,10) ri(i=1,2,⋅⋅⋅,10)对应的最优切割方案:
i i i 1 2 3 4 5 6 7 8 9 10 切割方式 1 2 3 2+2 2+3 6 1+6 2+6 3+6 10 r i r_i ri 1 5 8 10 13 17 18 22 25 30 更一般地,对于 r n = max ( p n , r 1 + r n − 1 , r 2 + r n − 2 , ⋅ ⋅ ⋅ , r n − 1 + r 1 ) r_n=\max(p_n,r_1+r_{n-1},r_2+r_{n-2},···,r_{n-1}+r_1) rn=max(pn,r1+rn−1,r2+rn−2,⋅⋅⋅,rn−1+r1)
-
求解思路:
- 解法一:先将钢条切成两条,有n-1种方案,每一种方案的最优解都等于两个子钢条的最优解。我们从这n-1个伪最优解再挑出最优的解
- 解法二:将长度未n的钢条分解为左边开始的一段,以及剩余部分继续分解的结果。
-
最优子结构:问题的最优解由相关子问题的最优解组合而成,而这些子问题可以独立求解。钢条切割问题即满足最优子结构性质。
-
朴素递归算法求解最优钢条切割问题
- 伪代码:CUT-ROD(p, n):自顶而下的递归实现,p为价格数组
if n == 0 return 0 q = 负无穷 for i = 1 to n q=max(q,p[i]+CUT-ROD(p, n-i)) return q
- python代码:
def cut_rod(p, n): if n == 0: return 0 q = -1 for i in range(1, n+1): q = max(q, p[i-1] + cut_rod(p, n-i)) return q p = [1, 5, 8, 9, 10, 17, 17, 20, 24, 30] n = 5 print(cut_rod(p, n))
- CUT-ROD的缺陷:n每增加1,程序运行时间差不多就会增加1倍
-
使用动态规划方法求解最优钢条切割问题
-
思路:仔细安排子问题求解顺序,对每个子问题只求解一次,并将结果保存下来。如果随后再次需要此子问题的解,只需查找保存的结果。
-
实现方法:
-
带备忘的自顶而下法:仍按递归形式编写,但过程会保存每个子问题的解。当需要一个子问题的解时,首先检查是否已经保存过此解。如果是,则直接返回保存的值:否则按通常方式计算这个子问题。
-
伪代码:
-
MEMORIZED-CUT-ROD(p, n):生成备忘录
let r[0..n] be a new array for i = 0 to n r[i] = 负无穷 return MEMORIZED-CUT-AUX(p, n, r)
-
MEMORIZED-CUT-AUX(p, n, r)
if r[n] >= 0 return r[n] if n == 0 q = 0 else q = 负无穷 for i = 1 to n q = max(q, p[i] + MEMORIZED-CUT-AUX(p, n-i, r)) r[n] = q return q
-
-
python代码:
def memorized_cut_rod(p, n): r = [-1] * (n+1) return memorized_cut_aux(p, n, r) def memorized_cut_aux(p, n, r): q = -1 if r[n] >= 0: return r[n] if n == 0: q = 0 else: for i in range(1, n+1): q = max(q, p[i-1] + memorized_cut_aux(p, n-i, r)) r[n] = q return q p = [1, 5, 8, 9, 10, 17, 17, 20, 24, 30] n = 8 print(memorized_cut_rod(p, n))
-
运行时间: θ ( n 2 ) \theta(n^2) θ(n2)
-
-
自底而上法:任何子问题的求解都依赖于“更小的”子问题的求解。因而我们将子问题按规模排序,按由小到大的顺序进行求解
-
伪代码:BOTTOM-UP-CUT-ROD(p, n)
let r[0..n] be a new array r[0] = 0 for j = 0 to n q = 负无穷 for i = 1 to j q = max(q, p[i] + r[j-i]) r[j]=q return r[n]
-
python代码:
def memorized_cut_aux(p, n): r = ['']*(n+1) for j in range(0, n+1): q = 0 for i in range(1, j+1): q = max(q, p[i-1] + r[j-i]) r[j] = q return r[n] p = [1, 5, 8, 9, 10, 17, 17, 20, 24, 30] n = 8 print(memorized_cut_aux(p, n))
-
运行时间: θ ( n 2 ) \theta(n^2) θ(n2)
-
-
-
-
重构解(保存最优切割方案)
-
伪代码:EXTENDED-BOTTOM-UP-CUT-ROD(p, n)
let r[0..n] be a new array r[0] = 0 for j = 0 to n q = 负无穷 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
-
python代码:
def memorized_cut_aux(p, n): r = ['']*(n+1) s = ['']*(n+1) for j in range(0, n+1): q = 0 for i in range(1, j+1): if q < p[i-1] + r[j-i]: s[j-1] = i q = max(q, p[i-1] + r[j-i]) r[j] = q return r[n],s p = [1, 5, 8, 9, 10, 17, 17, 20, 24, 30] n = 9 print(memorized_cut_aux(p, n))
-