中心思想
动态规划(dynamic programming,简称dp)与分治方法相比而言:
类似点在于都是通过组合子问题的解来求解原问题。
不同点在于分治方法将问题划分为互不相交的子问题,递归的求解子问题;而动态规划在于子问题重叠的情况,不同子问题的解是递归进行的,反复的求解公共子问题。
动态规划的主题在于对每个子问题只求解一次并作记录。
动态规划的应用场景在于求解最优化问题(optimization problem)。
动态规划算法步骤:
- 刻画一个最优解的结构特征
- 递归地定义最优解
- 计算最优解的值,这里一般用自底向上的方法
- 利用计算出的信息构造一个最优解
举个栗子
栗子1)钢条切割
输入源:钢条长度n
以及每个尺寸的钢条单价pi
目标:价格rn
最大
思路
先考虑当两截(不切属于特殊的两截)最优切割收益:
rn = max(pn, r1 + rn-1 + r2 + rn-2, ..., rn-1 + r1)
上式可以用反证法证明确实在此时取到最大值(略)。
切割两截后的两根钢条组合满足最优子结构(optional substructure)性质:问题得最优解由相关子问题的最优解组合而成,而这些子问题可以独立求解。
这种思路异曲同工的变种是,假设从左边(右边也随意)割下长度为i的一段,只对n-i的一段继续进行切割(递归),左边不用进行切割。这样的好处是很明显的,在一层级上只做一个方向递归调用,粗略来说可以减少一半次数的函数调用。
问题可以如此描述:
第一段长度为n,收益为pn,剩余部分长度为0(左边,不用切割),对应的收益为r0,可以得到新的公式: rn = max(pi + rn-i) (1 <= i <= n)
自顶向下的递归实现 (问题规模由大变小)
伪代码如下:
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
上述算法效率在输入规模大的时候就会变成渣渣,原因很简单,计算机在不断重复计算一个已经计算过的问题。
使用动态规划求解
主题是空间换时间,时空权衡(time-memory trade-off)。
- 带备忘的自顶向下法(top-down with memoization)。自然递归(就是正常人的想法,大问题变小问题)编写。每个过程保存一个子问题的解。在解的过程判断此子问题是否已经解过。
- 自底向上法(bottom-up method)。将子问题按规模排序,按由小至大的顺序进行求解。当解某个子问题所涉及更小的问题都已经解答完毕,可以直接使用(不用做判断是不是解答过了,省略了if判断)。
自顶向下伪代码:
MEMOIZED-CUT-ROD(p, n)
let r[0..n] be a new array
for i = 0 to n
r[i] = -∞
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 = -∞
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 = -∞
for i = 1 to j
q = max(q, p[i] + r[j-i])
r[j] = q
return r[n]
重构解
扩展版本,保存最优解的切割方法
EXTENDED-BOTTOM-UP-CUT-ROD(p, n)
let r[0..n] and s[0..n] be new array
r[0] = 0
for j = 1 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
PRINT-CUT-ROD-SOLUTION(p, n)
(r, s) = EXTENDED-BOTTOM-UP-CUT-ROD(p, n)
while n > n
print s[n]
n = n - s[n]
Swift实现
注:自底向上是需要排序,因为本栗中,价格跟尺寸长短成正比,故已经为非递减序列~
func extendedBottomUpCutRod(priceList: [Int], rotLength: Int) -> (result: [Int], solution: [Int]) {
var result = Array (count: rotLength + 1 , repeatedValue: 0 )
var solution = Array (count: rotLength + 1 , repeatedValue: 0 )
for var j = 1; j <= rotLength; ++j {
var optNext = -10000
for var i = 1; i <= j; i++ {
if optNext < priceList[i] + result[j - i] {
optNext = priceList[i] + result[j - i]
solution[j] = i
}
}
result[j] = optNext
}
return (result, solution)
}
func printCutRodSolution(priceList: [Int], var rotLength: Int) {
var result: [Int]
var solution: [Int]
(result, solution) = extendedBottomUpCutRod(priceList, rotLength)
while rotLength > 0 {
println(solution[rotLength])
rotLength = rotLength - solution[rotLength]
}
}
var priceArray: [Int] = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30]
printCutRodSolution(priceArray, 4)
栗子2)最长公共子序列
输入源:数组X, 数组Y
目标:求两数组最长公共交集子数组
遇到最长公共子序列问题(longest-common-subsequence problem)时,可以毫不犹豫的使用dp来解决LCS问题。
思路
DP问题步骤1:刻画一个最优解的结构特征
暴力求解的效率很可怕,假如X相对Y较短,长度为m,那么其子序列有2^m个。也就是说解的可能有2^m个。输入数组规模大了自然就呵呵了。。
首先看LCS问题是否具有最优子结构性质。LCS的子问题(小规模)自然考虑是两个序列的前缀【注:为嘛不是后缀呢?因为前缀都不同哪来的必要考虑后缀。。。】。
各种反证法是可以证明LCS问题是具有最优子结构,证明略了。。不是搞数学。。
LCS最优子结构的特征如下:
如果X = <x1, x2, ..., xm>,Y = <y1, y2, ..., yn>是两个序列, Z = <z1, z2, ..., zk>是X与Y的任意LCS,则
- 如果xm = yn, 那么zk = xm = yn并且Zk-1是Xm-1和Yn-1的一个LCS。
- 如果xm != yn, 那么zk != xm意味着Z是Xm-1和Y的一个LCS。【注:因为LCS中必定不包含xm,问题规模缩减,X序列成员剔除xm,下同】
- 如果xm != yn, 那么zk != yn意味着Z是X和Yn-1的一个LCS。
不断的1,2,3可以获得一个解(递归解)
DP问题步骤2:求递归解
根据LCS最优子结构的性质1,2,3可以有如下公式(递归解):
DP问题步骤3:自底向上计算最优解
整一个二维数组c[0..m, 0..n]来保存c[i, j]的解。按行主次序(即外围for循环为1到m),还需要一个辅助列表b[1..m, 1..n]来帮住构造最优解。b[i, j]存的枚举型可以记录在计算c[i, j]时的子问题最优解。c[m, n]保存了X和Y的LCS长度。
DP问题步骤4:给出最优解 上伪代码
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 // 不从0开始的原因,写一个二维矩阵就懂了
c[i, 0] = 0
for j = 0 to n
c[0, j] = 0
for i = 1 to m
for j = 1 to n
if xi == yi
c[i, j] = c[i-1, j-1] + 1
b[i, j] = left-top // left-top枚举值
elseif c[i-1, j] >= c[i, j-1]
c[i, j] = c[i-1, j]
b[i, j] = top // top枚举值
else
c[i, j] = c[i, j-1]
b[i, j] = left // left枚举值
return c and b
通过表b我们可以快速的查阅追踪出X和Y的LCS。打印LCS伪代码如下:
PRINT-LCS(b, X, i, j) // 公共子序列X,Y皆可
if i == 0 or j == 0
return
if b[i, j] == left-top
PRINT-LCS(b, X, i-1, j-1)
print xi
elseif b[i, j] = top
PRINT-LCS(b, X, i-1, j)
else
PRINT-LCS(b, X, i, j-1)
算法改进
实际上我们可以在常数时间内判断出c[i, j]与c[i-1, j-1],c[i-1, j],c[i, j-1]三者之间的关系。故可以完全不需要辅助表b。
Swift实现
// c[i, j]与c[i-1, j-1],c[i-1, j],c[i, j-1]三者之间的关系枚举
enum LCSDirection: Int {
case left_top = 0
case top = 1
case left = 2
}
// 二维数组
struct Matrix {
let rows: Int, columns: Int
var grid: [Int]
init(rows: Int, columns: Int) {
self.rows = rows
self.columns = columns
grid = Array(count: rows * columns, repeatedValue: -10000)
}
func indexIsValidForRow(row: Int, column: Int) -> Bool {
return row >= 0 && row < rows && column >= 0 && column < columns
}
subscript(row: Int, column: Int) -> Int {
get {
assert(indexIsValidForRow(row, column: column), "Index out of range")
return grid[(row * columns) + column]
}
set {
assert(indexIsValidForRow(row, column: column), "Index out of range")
grid[(row * columns) + column] = newValue
}
}
}
func LCSLength(var rowArray: [Int], var columnArray: [Int]) -> (b: Matrix, c: Matrix){
var m = rowArray.count
var n = columnArray.count
var b: Matrix = Matrix(rows: m+1, columns: n+1)
var c: Matrix = Matrix(rows: m+1, columns: n+1)
for var i = 1; i <= m; i++ {
c[i, 0] = 0
}
for var j = 0; j <= n; j++ {
c[0, j] = 0
}
for var i = 1; i <= m; i++ {
for var j = 1; j <= n; j++ {
if rowArray[i-1] == columnArray[j-1] {
c[i, j] = c[i-1, j-1] + 1
b[i, j] = LCSDirection.left_top.rawValue
} else if c[i-1, j] >= c[i, j-1] {
c[i, j] = c[i-1, j]
b[i, j] = LCSDirection.top.rawValue
} else {
c[i, j] = c[i, j-1]
b[i, j] = LCSDirection.left.rawValue
}
}
}
return (b, c)
}
func printLCS(b: Matrix, rowArray: [Int], rowIndex: Int, columnIndex: Int) {
if rowIndex == 0 || columnIndex == 0 { return }
if b[rowIndex, columnIndex] == LCSDirection.left_top.rawValue {
printLCS(b, rowArray, rowIndex-1, columnIndex-1)
println(rowArray[rowIndex-1])
} else if b[rowIndex, columnIndex] == LCSDirection.top.rawValue {
printLCS(b, rowArray, rowIndex-1, columnIndex)
} else {
printLCS(b, rowArray, rowIndex, columnIndex-1)
}
}
var A: [Int] = [1, 2, 3, 2, 4, 1, 2]
var B: [Int] = [2, 4, 3, 1, 2, 1]
var bRecord: Matrix
var cCommon: Matrix
(bRecord, cCommon) = LCSLength(A, B)
printLCS(bRecord, A, A.count, B.count)
栗子3)最优二叉搜索树
续
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。