动态规划

92 阅读6分钟

设计思想

已知起点集合 { S1,S2,,Sn }\{\ S_1 , S_2 , \cdots, S_n\ \} , 终点集合 { T1,T2,,Tm }\{\ T_1 , T_2 , \cdots , T_m\ \} 以及中间点集、边集, 求从始点到终点的最短路径

image.png

假设起点或终点有 mm 个, 有 nn 层, 每个节点都有两条路可走, 那么使用 穷举 时路径大概有 O(m2n)O(m2^n)

动态规划思想

从终点往前推, 子问题的终点不变, 起点前移

image.png

记录 CTC \rightarrow T 子问题的最短路径( 各节点 ), 如 C1T1,C2T3,C3T3,C4T4C_1 \rightarrow T_1, C_2 \rightarrow T_3, C_3 \rightarrow T_3, C_4 \rightarrow T_4

子问题起始点前移, 记录 BTB \rightarrow T 的最短路径, 如 B2C1T1B_2 \rightarrow C_1 \rightarrow T_1

当前子问题的最优解根据上一个子问题的最优解而定, 即 B2C1T1B_2 \rightarrow C_1 \rightarrow T_1 是基于最短路径 C1T1C_1 \rightarrow T_1 , 以此类推

最后一步最大的子问题即为原始问题

优化原则

一个最优决策序列的 任何子序列 本身一定是相对于子序列的初始和结束状态的 最优 的决策序列

反例

求总长模 10 的最小路径

image.png

454 \rightarrow 5 路径模 10 最小的是 u,2u,2 ( 2 % 10 小于 5 % 10 )

353 \rightarrow 5 路径模 10 最小的是 u,4u,4 ( 4 % 10 小于 7 % 10 )

252 \rightarrow 5 路径模 10 最小的是 u,6u,6 ( 6 % 10 小于 9 % 10 )

151 \rightarrow 5 路径模 10 最小的是 d,1d,1 ( 11 % 10 小于 8 % 10 )

事实上虽然 454 \rightarrow 5 是子序列最优解, 但是 353 \rightarrow 5 已经不是子序列的最优解了, 此时不能使用动态规划

uuupup , dddowndown , 指代路径方向

设计要素

矩阵乘法

A1,A2,,AnA_1 , A_2 , \cdots , A_n 为矩阵序列, AiA_iPi1×PiP_{i-1}×P_i 阶矩阵 , i=1,2,,ni = 1, 2, \cdots , n , 确定乘法顺序使得元素 相乘的总次数 最少

输入 P=<10,100,5,50>P=<10, 100, 5, 50> 代表 A1=10×100A_1=10×100 矩阵, A2=100×5A_2=100×5 , A3=5×50A_3=5×50

(A1A2)A3:10×100×5+10×5×50=7500(A_1A_2)A_3 : 10 × 100 × 5 + 10 × 5 × 50 = 7500
A1(A2A3):10×100×50+100×5×50=75000A_1(A_2A_3): 10 × 100 × 50 + 100 × 5 × 50 = 75000

枚举算法

n 个矩阵连乘, 使用枚举法时递推公式为

P(n)={1n=1k=1n1P(k)P(nk)n>1P(n) = \begin{cases} 1 & n = 1 \\ \sum_{k=1}^{n-1}P(k)P(n-k) & n \gt 1 \end{cases}

将矩阵划分为左 k 个和右 n-k 个单独计算加括号方式数

P(n+1)=C(n)=1n+1C2nn=Ω(4nn32)P(n+1) = C(n) = \frac{1}{n+1}C_{2n}^n = \Omega\left(\frac{4^n}{n^{\frac{3}{2}}}\right)

动态规划算法

m[i,j]m[i,j] 表示 AiAjA_i \thicksim A_j 的乘积数, 则

m[i,j]={0i=jminik<j(m[i,k]+m[k+1,j]+Pi1PkPj)i<jm[i,j] = \begin{cases} 0 & i = j \\ \underset{i\le k \lt j}{min}(m[i,k] + m[k+1, j] + P_{i-1}P_kP_j) & i < j \end{cases}

递归实现

if i == j then
    return 0

m[i, j] = ∞

for k = i + 1 to j − 1 do
    q = RecurMatrixChain(P, i, k) + RecurMatrixChain(P, k+1, j) + p[i−1] * p[k] * p[j]
    
    if q < m[i, j] then
        m[i, j] = q

return m[i, j]
T(n){O(1)n=11+k=1n1(T(k)+T(nk)+1)n<1T(n) \ge \begin{cases} O(1) & n = 1 \\ 1 + \sum_{k=1}^{n-1}(T(k) + T(n-k) + 1) & n \lt 1 \end{cases}

n>1n>1 时有

T(n)n+k=1n1T(k)+k=1n1T(nk)=n+2k=1n1T(k)T(n)\ge n + \sum_{k=1}^{n-1}T(k) + \sum_{k=1}^{n-1}T(n-k)=n+2\sum_{k=1}^{n-1}T(k)

根据数学归纳法可证明

T(n)2n1=Ω(2n)T(n)\ge 2^{n-1} = \Omega(2^n)

迭代实现

每个子问题只计算一次, 从最小的子问题算起

使用 备忘录 记录计算过的结果

for r = 2 to n do
    for i = 1 to n − r + 1 do
        j = i + r − 1 // 2 ~ n
        m[i, j] = ∞
        
        for k = i + 1 to j − 1 do // i + 1 ~ j - 1 ( 不含起始和终点 )
            t = m[i, k] + m[k + 1, j] + p[i − 1] * p[k] * p[j]
            
            if t < m[i, j] then 
                m[i, j] = t

return m[1][n]

复杂度

O(n3)O(n^3)

比较

递归实现

  • 时间复杂性高, 空间消耗较小

  • 子问题被多次重复计算

  • 子问题计算次数呈 指数 增长

迭代实现

  • 时间复杂性较低, 空间消耗多

  • 每个子问题只计算一次

  • 子问题的计算随问题规模呈 多项式 增长

案例

投资问题

mm 元钱, nn 项投资, fi(x)f_i(x) 为将 xx 元投入第 ii 个项目的效益, 求

max f1(x1)+f2(x2)++fn(xn)max\ f_1(x_1) + f_2(x_2) + \cdots + f_n(x_n)
x1+x2++xn=mx_1 + x_2 + \cdots + x_n = m

image.png

Fk(x)F_k(x) 表示 xx 元钱投给前 kk 个项目的最大效益

Fk(x)=max0xkx fk(xk)+Fk1(xxk)F_k(x) = \underset{0 \le x_k \le x}{max}\ f_k(x_k) + F_{k-1}(x-x_k)
F1(x)=f1(x)F_1(x) = f_1(x)

k=1k=1 , 投资 151 \thicksim 5 元时

F1(1)=11,F1(2)=12,F1(3)=13,F1(4)=14,F1(5)=15F_1(1)=11, F_1(2)=12, F_1(3)=13, F_1(4)=14, F_1(5)=15

k=2k=2 时, 投资 11 元时

F2(1)=max f1(1),f2(1)=11F_2(1)=max\ f_1(1),f_2(1) = 11

k=2k=2 时, 投资 22 元时

F2(2)=max f2(2),F1(1)+f2(1),F1(2)=12F_2(2)=max\ f_2(2), F_1(1)+f_2(1), F_1(2)=12

以此类推

// k = 1 的情况
for j = 1 to m do
    F[1][j] = f[1][j]

// k >= 2 的情况
for k = 2 to n do
    for i = 1 to m do // 前 k 个项目总投资 1 ~ m 元的情况
        max = 0
        
        for j = 0 to i do // 第 k 个项目投资 j 元的情况
            if f[k][j] + F[k - 1][i - j] > max then
                max = f[k][j] + F[k - 1][i - j]

            F[k][i] = max

时间复杂度

W(n)=O(nm2)W(n)=O(nm^2)

背包问题

一个旅行者准备随身携带一个背包, 可以放入背包的物品有 nn 种, 每种物品的重量和价值分别为 wjw_j , vjv_j , 如果背包的最大重量限制是 bb , 怎样选择放入背包的物品以使得背包的价值最大?

假设 Fk(y)F_k(y) 为装前 kk 种物品, 总重不超过 yy , 背包的最大价值, 则

Fk(y)=max0<xk<y/wk Fk1(yxkwk)+xkvkF_k(y) = \underset{0<x_k< \lfloor y/w_k\rfloor}{max}\ F_{k-1}(y-x_kw_k) + x_kv_k

另一种更优的方式, 即判断加和不加时物品的总价值

Fk(y)=max Fk1(y),Fk(ywk)+vkF_k(y) = max\ F_{k-1}(y), F_k(y-w_k) + v_k

image.png

for j = 0 to c do
    m[0][j] = 0 // 没有物品时放不了任何东西

for j = 1 to n do
    m[j][0] = 0 // 最大重量为 0 时放不了任何东西

for k = 1 to n do // 遍历前 k 个物品
    for y = 1 to c do
        if w[k] > y then
            // 不够装了, 则不装
            m[k][y] = m[k - 1][y]
        else
            // 判断装和不装谁的价值更大
            m[k][y] = max(m[k - 1][y], m[k - 1][y - w[k]] + v[k])

// 求放置物品的情况
for k = n to 1 do
    if m[k][c] > m[k - 1][c] then
        x[k] = 1
        c -= w[k]
    else
        x[k] = 0

return m[n, c]

时间复杂度

O(n2)O(n^2)

最长公共子序列(LCS)

设序列 X,ZX, Z , 若存在一个严格递增的下标序列 <i1,i2,,ik><i_1, i_2, \cdots, i_k> 使得 zj=xij,j=1,2,,kz_j = x_{ij}, j=1,2,\cdots,k , 称 ZZXX 的子序列

Z={B,C,D,B}Z=\{B,C,D,B\} , X={A,B,C,B,D,A,B}X=\{A,B,C,B,D,A,B\} , 严格递增的下标为 <2,3,5,7><2,3,5,7>

穷举

X={x1,x2,,xm}X=\{x_1,x_2,\cdots,x_m\} 以及 Y={y1,y2,,yn}Y=\{y_1,y_2,\cdots,y_n\}

那么 XX 的子序列有 2m2^m 个 ( 元素出现或不出现在子序列中 ) , 同理 YY 的子序列有 2n2^n

判断最长公共子序列则需要 O(2m×2n)O(2^m×2^n) 的时间复杂度

动态规划

  • xm=ynx_m=y_n , 则 zk=xm=ynz_k=x_m=y_n , Zk1Z_{k-1}Xm1X_{m-1}Yn1Y_{n-1} 的 LCS

  • xmynx_m \neq y_n , zkxmz_k\neq x_m , 则 ZZXm1X_{m-1}YY 的 LCS

  • xmynx_m \neq y_n , zkynz_k\neq y_n , 则 ZZXXYn1Y_{n-1} 的 LCS

递推方程

c[i,j]={0i=0 or j=0c[i1][j1]+1i,j>0,xi=yimax c[i,j1],c[i1,j]i,j>0,xiyic[i,j]=\begin{cases} 0 & i = 0\ or\ j = 0 \\ c[i-1][j-1] + 1 & i,j > 0, x_i = y_i \\ max\ c[i,j-1], c[i-1,j] & i,j>0, x_i\neq y_i \end{cases}
// 边界情况
for i = 1 to m do
    c[0][i] = 0
    c[i][0] = 0

for i = 1 to m do
    for j = 1 to n do
        if x[i] = y[j] then
            c[i][j] = c[i - 1][j - 1] + 1
        else
            c[i][j] = max(c[i - 1][j], c[i][j - 1])

// 获取最长子序列
i = m
j = n
while i > 0 and j > 0
    if x[i] = y[j] then
        res = x[i] + res
        i--
        j--
    else if c[i][j - 1] <= c[i - 1][j]
        i--
    else
        j--

return c[m][n]

计算时间复杂度 Θ(nm)\Theta(nm)

构造解时间复杂度 Θ(m+n)\Theta(m+n)

空间复杂度 Θ(mn)\Theta(mn)

最大子段和

给定 nn 个整数(可以为负数)的序列 <a1,a2,,an><a_1, a_2, \cdots, a_n> , 求

max( 0,max1ijnk=1jak )max\left(\ 0, \underset{1\le i \le j \le n}{max} \sum_{k=1}^j a_k \ \right)

要求 kk 是连续的

顺序求和

sum = 0
begin = 0
end = 0

for i = 1 to n do
    for j = i to n do
        s = 0
        
        // 计算 i ~ j 之间子段的和
        for k = i to j do
            s += a[k]
        
        if s > sum then
            sum = s
            
            // 记录字段的起始和结束下标
            begin = i
            end = j

时间复杂度

O(n3)O(n^3)

分治策略

分别计算左半部分的最大子段和 ls , 右半部分的最大字段和 rs , 以及跨越两部分的最大字段和 sum , 判断谁更大

if left = right then
    return max(0, a[left])

mid = (left + right) // 2
ls = maxSubSum(a, left, mid)
rs = maxSubSum(a, mid + 1, right)

for i = left to mid do
    s1 += a[i]

for i = mid + 1 to right do
    s2 += a[i]

sum = s1 + s2

if ls > sum then
    sum = ls

if rs > sum then
    sum = rs

return sum

时间复杂度

O(nlogn)O(nlogn)

动态规划

c[i]c[i]a[i]a[i] 必须在子段末尾的子段和

c[i+1]=max c[i]+a[i+1],a[i+1]c[i+1]=max\ c[i] + a[i+1], a[i+1]

如果 c[i]>0c[i] > 0 , 无论 a[i+1]a[i + 1] 是正是负, c[i]+a[i+1]c[i] + a[i + 1] 一定大于 a[i+1]a[i + 1] , 故最大子段和是 c[i]+a[i+1]c[i] + a[i + 1], 反之, c[i]+a[i+1]c[i] + a[i + 1] 一定小于 a[i+1]a[i + 1] , 最大字段和取 a[i+1]a[i + 1]

for i = 1 to n do
    c[i] = max(a[i], c[i - 1] + a[i])

sum = 0
for i = 1 to n do
    sum = max(sum, c[i]) // 在所有子段和中找最大的

return sum

时间复杂度和空间复杂度

O(n)O(n)

进一步优化, 空间复杂度 O(1)O(1)

sum = 0
max = 0

for i = 0 to n do
    if sum > 0 then
        sum += a[i]
    else
        sum = a[i]
    
    if sum > max then
        max = sum

return max

最优二分检索树

设集合 S={x1,x2,,xn}S = \{x_1,x_2,\cdots,x_n\} 为有序集 , 将这些存储在一棵二叉搜索树上

在二叉搜索树中查找 xx 并返回, 存在两种情况

  • 在二叉树中找到对应节点, x=xix=x_i

  • 在二叉树叶节点中确定区间, x(xi,xi+1)x\in(x_i, x_{i+1})

设查找 xx 时, 找到元素的概率为 bib_i , 找到区间的概率为 aia_i

SS 的存取概率分布为

P=(a0,b1,a1,,bn,an)P=(a_0, b_1, a_1, \cdots, b_n, a_n)

因为有的元素在 (x0,x1)(x_0, x_1) 之间, 因此多了一个 a0a_0 , 其中 x0=x_0=-\infty , xn+1=+x_{n+1}=+\infty

平均比较次数, 其中 ci,dic_i,d_i 指查找深度

注意深度从 0 还是从 1 开始, 下面的公式从 0 开始

p=i=1nbi(1+ci)+j=0najdjp=\sum_{i=1}^n b_i(1 + c_i)+\sum_{j=0}^n a_jd_j

image.png

image.png

问题描述

给定数据集 SS 和相关存取概率分布 PP , 求一棵最优的(即平均比较次数最少的)二分检索树?

解决思路

子数据集

s[i,j]=<xi,xi+1,,xj>s[i,j] = <x_i, x_{i+1}, \cdots, x_j>

与子数据集对应的存取概率分布

p[i,j]=<ai1,bi,ai,,bj,aj>p[i,j] = <a_{i-1}, b_i, a_i, \cdots, b_j, a_j>

p[i,j]p[i,j] 中存取概率和

w[i,j]=p=i1jap+q=ijbqw[i,j]=\sum_{p=i-1}^j a_p + \sum_{q=i}^j b_q

m[i,j]m[i,j] 指平均比较次数, 因此递推公式为

m[i,j]=minikj( m[i,k1]+m[k+1,j] )+w[i,j]m[i,j]=\underset{i\le k\le j}{min}(\ m[i,k-1]+m[k+1,j]\ ) + w[i,j]

合并为一棵新树后, 左右子树的深度都增加了 11, 因此加上 w[i,j]w[i,j]

for i = 0 to n do
    w[i + 1][i] = a[i]
    m[i + 1][i] = 0

for r = 0 to n do
    for i = 1 to n - r do
        j = i + r
        
        w[i][j] = w[i][j - 1] + a[j] + b[j]
        m[i][j] = m[i + 1][j]
        s[i][j] = i
        
        for k = i + 1 to j do
            t = m[i][k - 1] + m[k + 1][j]
            
            if t < m[i][j] then
                m[i][j] = t
                s[i][j] = k // 子树根
    
    m[i][j] += w[i][j]

时间复杂度

O(n3)O(n^3)

生物信息学

RNA 一级结构: 由字母 A,C,G,U 标记的核苷酸构成的一条链

RNA 二级结构: 核苷酸相互匹配

匹配原则

  • 配对 U-A,C-G

  • 不允许交叉

    image.png

  • 每个核苷酸只能参加一个配对

  • 末端不出现“尖角” , 即 iijj 配对, 要求 ij4i \le j - 4

    image.png

问题描述

给定 RNA 的一级结构, 由 A,U,C,G 构成的长为 n 的 序列, 寻找具有 最大匹配对数 的二级结构

解决思路

c[i,j]c[i,j] 是序列 s[i,j]s[i,j] 的最大匹配数, 递推公式如下

c[i,j]=max( c[i,j1],maxik<j4(1+c[i,k1]+c[k+1,j]) )c[i,j] = max(\ c[i,j-1], \underset{i\le k\lt j-4}{max}(1 + c[i, k-1] + c[k+1,j])\ )
c[i,k]=0   ji<5c[i,k]=0 \ \ \ j - i < 5

比较 i,ji,j 配对 ( 1+c[i,k1]+c[k+1,j]1 + c[i, k-1] + c[k+1,j] )和不配对 ( c[i,j1]c[i,j-1] ) 时谁的配对数更大

时间复杂度

O(n3)O(n^3)

序列比对

给定两个序列 S1S_1S2S_2 , 通过一系列字符编辑(插入、删除、替换)等操作, 将 S1S_1 转变成 S2S_2 , 完成这种转换所需要的 最少编辑操作个数 称为 编辑距离

例子

vintnervintner 转变成 writerswriters , 最少进行六次操作即可完成

image.png

解决思路

已知序列 s1[1,n]s_1[1,n]s2[1,m]s_2[1,m]

c[i,j]c[i,j] 为序列 s1[1,i]s_1[1,i]s2[1,j]s_2[1,j] 的编辑距离, 则递推公式为

c[i,j]=min( c[i1,j]+1,c[i,j1]+1,c[i1,j1]+t[i,j] )c[i,j] = min(\ c[i-1,j] + 1, c[i,j-1]+1,c[i-1,j-1]+t[i,j]\ )

分别对应删除、插入、替换操作

其中

t[i,j]={0s1[i]=s2[j]1s1[i]s2[j]t[i,j]=\begin{cases} 0 & s_1[i]=s_2[j] \\ 1 & s_1[i]\neq s_2[j] \end{cases}
c[0,j]=j,c[i,0]=ic[0,j]=j, c[i,0]=i

时间复杂度

O(nm)O(nm)

图像压缩

像素点灰度值 0~255 , 使用 8 位二进制数表示

像素点灰度值序列 <p1,p2,,pn>< p_1 , p_2 , \cdots , p_n > , pip_i 为第 ii 个像素点灰度值

变位压缩存储格式

<p1,p2,,pn>< p_1 , p_2 , \cdots, p_n > 分割 mmS1,S2,,SmS_1 , S_2 , \cdots , S_m

段头 1111 位, 包括 SiS_i 的最大位数(使用 3 位二进制表示, 表示位数不超过 8), 以及段内元素个数(8 位, 表示数量不超过 256)

p=<10,12,15,255,1,2,1,1,2,2,1,1>p=<10,12,15,255,1,2,1,1,2,2,1,1>

可以分割成

S=<10,12,15>,<255>,<1,2,1,1,2,2,1,1>S=<10,12,15>,<255>,<1,2,1,1,2,2,1,1>

那么存储占用为

s=4×3+11+8+11+2×8+11=69s = 4 × 3 + 11 + 8 + 11 + 2 × 8 + 11 = 69

问题描述

给定像素序列 <p1,p2,,pn>< p_1 , p_2 , \cdots, p_n > , 确定最优分段

解决思路

s[i]s[i] 是像素序列 <p1,p2,,pi>< p_1 , p_2 , \cdots, p_i > 最优分段所需的存储位数

s[i]=min1kmin{i,256}s[ik]+k×b[ik+1,i]+11s[i]=\underset{1\le k \le min\{i,256\}}{min} s[i-k]+k×b[i-k+1,i] + 11

kk 是指往后将多少个元素归为一类, 最多不超过 256256 , 即 SiS_i 元素个数上限

b[i,j]=log(maxikj pk+1)b[i,j]=\left\lceil log\left(\underset{i\le k\le j}{max}\ p_k+ 1\right) \right\rceil

<pi,,pj>< p_i , \cdots, p_j > 中最大值的位数

lmax = 256
header = 11
s[0] = 0

for i = 1 to n do
    // bmax 至少等于 pi 的位数
    b[i] = len(p[i]) 
    bmax = b[i]
    
    // 假设分段为 <..., p[i - 1]>, <p[i]>
    s[i] = s[i - 1] + bmax
    l[i] = 1
    
    // 往前归类, 如 <..., p[i - 2]>, <p[i - 1], p[i]>
    for k = 2 to min(i, lmax) do
        // 分组内单个元素最大占用变大
        if bmax < b[i - k + 1] then
            bmax = b[i - k + 1]
        
        // 反而使总的占用减小
        if s[i] > s[i - k] + k * bmax then
            s[i] = s[i - k] + k * bmax
            l[i] = k
     s[i] += header

image.png

时间复杂度

O(n)O(n)