动态规划问题
动态规划方法通常用来求解最优化问题。问题可以有很多可行解,每个解都有一个值,我们希望寻找具有最优值的解。这样的解为问题的一个最优解(an optimal solution),而不是最优解(the optimal solution),因为可能有多个解都达到最优值。
一、钢铁切割问题
给定一段长度为
n
英寸的钢条和一个价格表
- 解法一:
为了求解规模为 n 的原问题,可以先求解形式完全一样,但规模更小的子问题。即当完成首次切割后,我们将两段钢条看成两个独立的钢条切割问题。
通过组合两个相关子问题的最优解,并在所有可能的两段切割方案中选取组合收益最大者,构成原问题的最优解。
称钢条切割问题满足最优子结构性质:问题的最优解由相关子问题的最优解组合而成,而这些子问题可以独立求解。
- 解法二:
递归求解:将钢条从左边切割下长度为
即问题分解的方式为:将长度为 n 的钢条分解为左边开始一段,以及剩余部分继续分解的结果。
公式:
在此公式中,原问题的最优解只包含一个相关子问题(右端剩余部分)的解,而不是两个。
1. 自顶向下递归实现–解法二
Cut_rod(p, n)
if n == 0
return 0
q = -inf
for i = 1 to n
q = max(q, p[i] + Cut_rod(p, n-i))
return q
- 随着n的增大,程序运行时间越来越长,效率很低,工作量会爆炸性地增长;
- Cut_rod反复用相同的参数值对自身进行递归调用,即反复在求解相同的子问题;
如图:
运行时间: 2n ,为n的指数函数。
2. 动态规划方法求解
动态规划方法仔细安排求解顺序,对于每个子问题只求解一次,并将结果保存下来。
动态规划方法是付出额外的内存空间来节省计算时间,是典型的时空权衡的例子。
两种等价的实现方法:
带备忘的自顶向下法
仍然按照自然的递归形式编写过程,但过程中会保存每个子问题的解。
Memoized_cut_rod(p, n)
let r[0..n] be a new array
for i = 0 to n
r[i] = -inf
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 = -inf
for i = 1 to n
q = max(q, p[i] + Memoized_cut_rod_aux(p, n-i, r))
r[n] = q
return q
自底向上法
将子问题按规模排序,按由小到大的顺序进行求解。
Bottom-up-cut-rod(p,n)
let r[0..n] be a new array
r[0] = 0
for j = 1 to n
q = - inf
for i = 1 to j
q = max(q, p[i]+r[j-i])
r[j] = q
return r[n]
- 两种方法得到的算法具有相同的渐进运行时间;
- 自顶向下的方法并未真正地递归考察所有可能的子问题;
- 由于没有频繁的递归函数调用的开销,自底向上的方法的时间复杂性函数通常具有更小的系数。
3. 重构解
对2中的动态规划算法进行扩展,使之对每个子问题不仅保存最优收益值,还保存对应的切割方案。
其中
sj
保存钢条的切割长度;最大收益值
rj
。
Extended-bottom-up-cut-rod(p,n)
let r[0..n] and s[0..n] be new arrays
r[0] = 0
for j = 1 to n
q = - inf
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
长度为n的钢条的完整的最优切割方案:
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]
二、矩阵链乘法
矩阵链乘法中,在相乘的过程中加括号会对乘积运算的代价产生巨大的影响。
矩阵链乘法问题: 给定
n
个矩阵的链
利用动态规划的方法寻找最优括号化方案。
步骤1:最优括号化方案的结构特征
假设 AiAi+1⋯Aj 的最优括号化方案的分割点在 Ak 和 Ak+1 之间。那么继续对“前缀”子链 AiAi+1⋯Ak 进行括号化时,我们应该直接采用独立求解它时的最优化方案。
步骤2:一个递归求解方案
令 m[i,j] 来表示计算矩阵 Ai,j 所需标量乘法的最小值。
矩阵 Ai 的大小为 pi−1×pi
AiAI+1⋯Aj
最小代价括号化方案的递归求解公式:
步骤3:计算最优代价
自底向上方法实现:
Matrix_chain_order(p)
n = p.length - 1
let m[1..n, 1..n] and s[1..n-1,2..n] be new tables
for i = 1 to n
m[i,j] = 0
for l = 2 to n
for i = 1 to n-l+1
j = i + l - 1
m[i,j] = inf
for k = i to j-1
q = m[i,k] +m[k+1,j] + p_{i-1}p_{k}p_{j}
if q < m[i,j]
m[i,j] = q
s[i,j] = k
return m and s
时间代价:
O(n3)
空间代价:
Θ(n2)
算法图解
步骤4:构造最优解
输出最优括号化方案:
Print_optimal_parens(s,i,j)
if i == j
print "A"
else print "("
Print_optimal_parens(s,i,s[i,j])
Print_optimal_parens(s,s[i,j]+1,j)
print ")"
调用 Print_optimal_parens(s,1,6)输出括号化方案: ((A1(A2A3))((A4A5)A6))
带备忘的自顶向下法:
Memoized_matrix_chain(p)
n = p.length - 1
let m[1..n,1..n] be a new array
for i = 1 to n
for j = 1 to n
m[i,j] = inf
return Lookup_chain(m,p,1,n)
Lookup_chain(m,p,1,n)
if m[i,j] < inf
return m[i,j]
if i == j
m[i,j] = 0
else for k = i to j-1
q = Lookup_chain(m,p,i,k) + Lookup_chain(m,p,k+1,j) + p_{i-1}p_{k}p_{j}
if q < m[i,j]
m[i,j] = q
return m[i,j]
时间复杂度: O(n3)
三、最长公共子序列
Longest-common-subsequence problem.
最长公共子序列问题:给定两个序列
X=<x1,x2,⋯,xm>
和
Y=<y1,y2,⋯,yn>
,求
X
和
步骤1:刻画最长公共子序列的特征
如果用暴力搜索方法求解LCS问题,就要穷举X的所有子序列,对每个子序列检查它是否也是Y的子序列,运行时间为 2m 即为指数阶。
步骤2:一个递归解
定义 c[i,j] 表示 Xi 和 Yj 的LCS的长度。
根据LCS问题的最优子结构性质,可得如下公式:
步骤3:计算LCS的长度
使用动态规划方法自底向上计算。
过程Lcs_length 接受两个序列 X=<x1,x2,⋯,xm> 和 Y=<y1,y2,⋯,yn> 为输入。
它将 c[i,j] 的值保存在表c[0..m,0..n]中,并按行主次序计算表项。过程还维护一个表 b[1..m,1..n] ,帮助构造最优解。
b[i,j] 指向的表项对应计算 c[i,j] 时所选择的子问题最优解。过程返回表b和表c,c[m,n]保存了X和Y的LCS的长度。
Lcs_length(X,Y)
m = X.length
n = Y.length
let b[1..m,1..n] and c[0..m,0..n] be new tables
for i = 1 to m
c[i,0] = 0
for j = 1 to n
c[0,j] = 0
for i = 1 to m
for j = 1 to n
if x_{i} = y_{j}
c[i,j] = c[i-1, j-1]+1
b[i,j] = "^\" #表示图中指向左上的箭头
elseif c[i-1,j] >= c[i,j-1]
c[i,j] = c[i,j-1]
b[i,j] = "^|" #表示图中指向上的箭头
else c[i,j] = c[i,j-1]
b[i,j] = "<-" #表示图中指向左的箭头
return c and b
下图显示了Lcs_length对输入序列
X=<A,B,C,B,D,A,B>
和
Y=<B,D,C,A,B,A>
生成的结果。过程的运行时间为
Θ(mn)
,因为每个表项的计算时间为
Θ(1)
。
步骤4:构造LCS
可以用Lcs_length返回的表b快速构造 X=<x1,x2,⋯,xm> 和 Y=<y1,y2,⋯,yn> 的LCS,只需简单地从 b[m,n] 开始,并按照箭头方向追踪下去即可。
当在表项 b[i,j] 中,遇到一个“指向右上”的箭头时,意味着 xi=yj 是LCS的一个元素。
Print_lcs(b,X,i,j)
if i == 0 or j == 0
return
if b[i,j] == "^\"
Print_lcs(b,X,i-1,j-1)
elseif b[i,j] == "^|"
Print_lcs(b,X,i-1,j)
else
Print_lcs(b,X,i,j-1)
四、最优二叉搜索树
optimal binary search tree.
最优二叉搜索树问题:给定一个n个不同关键字的已排序的序列 K=<k1,k2,..,kn> ,用这些关键字构造一棵二叉搜索树。
对每个关键字
ki
,都有一个概率
pi
表示其搜索频率。有些要搜索的值可能不在K中,因此我们还有n+1个“伪关键字”
d0,d1,d2,⋯,dn
表示不在K中的值。
d0
表示所有小于
k1
的值,
dn
表示所有大于
kn
的值,
di
表示所有在
ki
和
ki+1
之间的值。
假定一次搜索的代价等于访问的节点数,即此次搜索找到的结点在T中的深度再加1。则一次搜索的期望代价为:
步骤1:最优二叉搜索树的结构
如果一棵最优二叉搜索树T有一棵包含关键字 ki,⋯,kj 的子树T’,那么T’必然是包含关键字 ki,⋯,kj 和伪关键字 di−1,⋯,dj 的子问题的最优解。
???没有明白这么表述的意义在哪里。
步骤2:一个递归算法
对于包含关键字
ki,⋯,kj
的子树,所有概率之和为:
w(i,j)=∑jl=ipl+∑jl=i−1ql
若
kr
为包含关键字
ki,⋯,kj
的最优二叉搜索树的根结点,公式:
e[i,j]的值给出了最优二叉搜索树的期望搜索代价。
步骤3:计算期望搜索代价
表
e[1..n+1,0..n]
来保存
e[i,j]
的值。第一维的下标上界为
n+1
而不是
n
,原因在于对于只包含伪关键字
同时还需要一个w[1..n+1,0..n]的表来保存w(i,j)避免每次计算e[i,j]时重新计算w(i,j).
令
w[i,i−1]=qi−1(1<=i<=n+1)
,对
j>=i
的情况,
Optimal_bst(p,q,n)
let e[1..n+1, 0..n], w[1..n+1, 0..n], and root[1..n, 1..n] be new tables
for i = 1 to n+1
e[i,i-1] = q_{i-1}
w[i,i-1] = q_{i-1}
for l = 1 to n
for i = 1 to n-l+1
j = i+l-1
e[i,j] = inf
w[i,j] = w[i,j-1] + p_{j} + q_{j}
for r = i to j
t = e[i,r-1] + e[r+1,j] +w[i,j]
if t < e[i,j]
e[i,j] = t
root[i,j] = r
return e and root
时间复杂度: O(n3)
作者: 大树先生
博客: http://blog.csdn.net/koala_tree
2017 年 09 月 06 日
本文为博主原创文章,未经本人允许不得转载。