中心思想

动态规划(dynamic programming,简称dp)与分治方法相比而言:

类似点在于都是通过组合子问题的解来求解原问题
不同点在于分治方法将问题划分为互不相交的子问题,递归的求解子问题;而动态规划在于子问题重叠的情况,不同子问题的解是递归进行的,反复的求解公共子问题。

动态规划的主题在于对每个子问题只求解一次并作记录。
动态规划的应用场景在于求解最优化问题(optimization problem)

动态规划算法步骤:

  1. 刻画一个最优解的结构特征
  2. 递归地定义最优解
  3. 计算最优解的值,这里一般用自底向上的方法
  4. 利用计算出的信息构造一个最优解

举个栗子

栗子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,则

  1. 如果xm = yn, 那么zk = xm = yn并且Zk-1是Xm-1和Yn-1的一个LCS。
  2. 如果xm != yn, 那么zk != xm意味着Z是Xm-1和Y的一个LCS。【注:因为LCS中必定不包含xm,问题规模缩减,X序列成员剔除xm,下同】
  3. 如果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)最优二叉搜索树


Cruise_Chan
729 声望71 粉丝

技能树点歪了...咋办