Algorithm Design and Analysis - Dynamic programming
Main message: 1. 把子问题的求解想象成多步求解过程 2. 子问题的最优解可以组合成原问题的最优解 3. Programming:tabular可以被用于避免子问题的重复计算
联动: Dynamic Programming 动态规划算法
Dynamic programming VS. Divide-and-conquer
动态规划算法一般被用于解决优化问题。需满足以下几个特点:
- 原问题可以被分解成更小的子问题
- 具有最优子结构性质,即最优解可以通过结合子问题的最优解得到。
与通用的分治法框架不同,动态规划算法通常枚举所有可能的分解策略。子问题的重复计算可以通过“programming”避免。
Matrix Chain Multiplication problem 矩阵的链式乘法
INPUT:A sequence of n matrices \(A_1, A_2 , ..., A_n\); matrix A i has dimension \(p_{i-1} \times p_i\)
OUTPUT:Fully parenthesizing the product \(A_1, A_2 , ..., A_n\) in a way to minimize the number of scalar multiplications. ***
\[A_1 = \left[\begin{matrix} 1 & 2 \end{matrix}\right] A_2 = \left[ \begin{matrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{matrix} \right] A_3 =\left[\begin{matrix} 1 & 2 & 3 & 4\\ 4 & 5 & 6 & 7\\7 & 8 & 9 & 10\end{matrix} \right]\]
Solutions:
\(((A_1)(A_2))(A_3) : (1 \times 2 \times 3) + (1 \times 3 \times 4)\)
\((A_1)((A_2)(A_3)) : (2 \times 3 \times 4) + (1 \times 2 \times 4)\)
我们要如何选取矩阵相乘的顺序来使乘的次数最小呢?
若枚举所有可能的解法,时间复杂度是指数级的。我们可以将其想象成一个多步决策,将加括号看成子问题。我们使用OPT(i,j)来表示子问题,原问题可以通过计算OPT(1,n)解决。由于最优子结构性质,我们可以通过计算子问题的最优解计算出原问题的最优解。
\[OPT(1,n) = OPT(1,k) + OPT(k+1,n) + p_1p_{k+1}p_{n+1}\] 我们可以通过枚举所有可能的解来获得第一步决策。
Trials
Trial 1
1 | RECURSIVE MATRIX CHAIN(i, j) |
最优解:RECURSIVE MATRIX CHAIN(1, n)
然而,这种方式需要花费指数时间\(O(n^2)\),且有大量的重复计算。我们可以开一个二维数组来存储subsolution,之后查表即可。
Trial 2
1 | MEMORIZE_MATRIX_CHAIN(i, j) |
原问题可以通过调用MEMORIZE_MATRIX_CHAIN(1, n)并 将所有OPT[i,j]设为NULL来解决。时间复杂度为\(O(n^3)\)
Trial 3
更快的一种解决方式:自底向上迭代 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18MATRIX CHAIN MULTIPLICATION(p 0 , p 1 , ..., p n )
for i = 1 to n do
OPT(i, i) = 0;
end for
for l = 2 to n do
for i = 1 to n − l + 1 do
j = i + l − 1;
OPT(i, j) = +∞;
for k = i to j − 1 do
q = OPT(i, k) + OPT(k + 1, j) + p i−1 p k p j ;
if q < OPT(i, j) then
OPT(i, j) = q;
S(i, j) = k;
end if
end for
end for
end for
return OPT(1, n);
解题顺序:红 \(\Rightarrow\) 绿 \(\Rightarrow\) 橙 \(\Rightarrow\) 蓝,自底而上,先列出所有子问题
step 1:
OPT[1,2],OPT[2,3],OPT[3,4]
step 2: \[OPT[1,3] = min \left\{ \begin{aligned} OPT[1,2] + OPT[3,3] + p_0 \times p_2 \times p_3 \\ OPT[1,1] + OPT[2,3] + p_0 \times p_1 \times p_3 \end{aligned} \right.\] Thus, SPLITTER[1,2] = 2.
\[OPT[1,3] = min \left\{ \begin{aligned} OPT[2,2] + OPT[3,4] + p_1 \times p_2 \times p_4 \\ OPT[2,3] + OPT[4,4] + p_1 \times p_3 \times p_4 \end{aligned} \right.\] Thus, SPLITTER[2,4] = 3.
step 3: \[OPT[1,3] = min \left\{ \begin{aligned} OPT[1,1] + OPT[2,4] + p_0 \times p_1 \times p_4 \\ OPT[1,2] + OPT[3,4] + p_0 \times p_2 \times p_4 \\ OPT[1,3] + OPT[4,4] + p_0 \times p_3 \times p_4 \end{aligned} \right.\] Thus, SPLITTER[1,4] = 3.
在此方法中,只有子问题的最优解被计算,因此没有经过遍历,不是指数级的复杂度。
Backtracking
从OPT[1,n]开始回溯每一个决策阶段。
Step 1: \((A_1A_2A_3)(A_4)\)
Step 2: \(((A_1A_2)(A_3))(A_4)\)
Step 3: \((((A_1)(A_2)(A_3))(A_4)\)
0/1 KNAPSACK problem 0/1背包问题
INPUT: A set of items \(S = \{1, 2, ..., n\}\). Item i has weight \(w_i\) and value \(v_i\) . A total weight limit W; OUTPUT: A subset of items to maximize the total value with total weight below W. *** 这里的0和1表示我们可以选择一个物品(1)或者放弃它(0),我们不能选择物品的一部分。
我们可以将将最优解表示为:\(OPT({1,2,...,i},w)\)
最优子结构: \[OPT({1,2,...,n},W) = max
\left\{\begin{array}{lr}OPT({1,2,...,n-1},W) \\OPT({1,2,...,n-1},W-w_n)
+ v_n\end{array}\right.\] 1
2
3
4
5
6
7
8
9Knapsack(n, W)
for w = 1 to W do
OPT[0, w] = 0;
end for
for i = 1 to n do
for w = 1 to W do
OPT[i, w] = max{OPT[i−1, w], vi +OPT[i−1, w−wi]};
end for
end for
step 1:
Initially all OPT[0, w] = 0.
step 2:
\[OPT[1, 2] = max\{ OPT[0, 2], OPT[0, 0] + V_1 \} =2\]
step 3:
\[OPT[2, 4] = max\{ OPT[1, 4], OPT[1, 2] + V_2\} =4\]
step 4:
\[OPT[3, 3] = max\{OPT[2, 3], OPT[2, 0] + V_3\} =3\]
backtracking
\[OPT[3, 6] = max{ OPT[2, 6](= 4), OPT[2, 3] + V_3 (= 2 + 3)} =5\]
Decision: Select item 3
\[OPT[2, 3] = max{ OPT[1, 3](= 2), OPT[1, 1] + V_2 (= 0 + 2)} =2\]
Decision: Select item 2
时间复杂度:\(O(nW)\)
若将items看做是集合(recursion over sets),将花费指数倍的时间;但是若将items先进行排序(recursion over sequences),花费的时间为\(O(nW)\)。
Sequence Alignment problem
Alignment is introduced to describe the generating process of an erroneous word from the correct word using a series of INS/DEL/MUTATION operations.
要比较两个序列,我们首先在合适的地方引入空格-。使得两个序列的长度相同,即\(|S' = T'|\)
对齐的字符可能有三种情况: 1. S'[i] = "-" : S'[i] is simply a DELETION of T'[i]. 2. T'[i] = "-" : S'[i] is simply an INSERTION. 3. Otherwise, S'[i] is a copy of T'[i] (with possible MUTATION).
我们可以用以下方式来给s(a,b)打分: 1. Match: +1 , e.g. s('C' ,'C' ) = 1. 2. Mismatch: -1, e.g. s('E' , 'A' ) = −1. 3. Insertion/Deletion: -3, e.g. s('C' , '-' ) = −3.
使用对齐,我们可以找到最相似的来源,并且同样两个序列,通过不同的插入空格的方式我们可以得到不同的值。
INPUT:Two sequences S and T, |S| = m, and |T| = n; OUTPUT:To identify an alignment of S and T that maximizes a pre-defined scoring function. *** 这里也有三种需要考虑的情况: 1. \(S_m\) comes from \(T_n\):S从T产生 2. \(S_m\) is an INSERTION:若S的最后一个字母多敲了 3. \(S_m\) comes from T[1..n − 1]:若S少敲了
note:蓝色框表示子问题
因此,我们可以将子问题的一般式设计为S的前缀(S[1..i])和T的的前缀(T[1..j])。最优解可以被表示为OPT(i,j).
\[OPT(i,j) = max
\left\{\begin{array}{lr}s(S_i,T_j) + OPT(i-1, j-1) \\s('-',T_j)
+ OPT(i, j-1) \\s(S_i,'-') + OPT(i-1,
j)\end{array}\right.\] 1
2
3
4
5
6
7
8
9
10
11
12
13
14Needleman-Wunsch(S, T)
for i = 0 to m; do \\简单的初始化,为了方便计算
OPT[i, 0] = −3 ∗ i;
end for
for j = 0 to n; do
OPT[0, j] = −3 ∗ j;
end for
for i = 1 to m do
for j = 1 to n do
\\从三个单元里各自取了一些分,取最大
OPT[i,j]=max{OPT[i−1,j−1]+s(Si,Tj),OPT[i-1,j]−3,OPT[i,j−1]−3};
end for
end for
return OPT[m, n] ;
之后通过回溯找到最优对齐方式(开一个表格方便回溯):
在实践中,有许多中对齐方式可以达到与最优比对相似的效果,被称作次优比对。次优比对可以分为两类:1)类似的次优比对,2)不同的次优比对。
为了找到类似的次优对齐,我们使用采样技术追溯动态编程矩阵,即,不是在每个步骤采用最高得分选项,而是基于三个选项的值进行概率选择。e.g.将4以一定的概率回到之前的三个点,找一百回,用聚类的算法找到聚类中心。(最robust)
优化
若输入的数据很大(e.g.输入了两篇文章)这个算法面临了三个问题: 1. 占用空间太大 2. 用时太长 3. Local
Space efficient algorithm:
Reducing the space requirement from O(mn) to O(m + n) (D. S. Hirschberg, 1975).
若只计算分数而不记录对齐信息,占用的空间很小。因为我们仅需占用2个数组。绿色数组:已填,蓝色数组:待填,一旦把蓝色的数组填完,绿色的数组就没用了。白色的部分都不需要存。
1 | Prefix Space Efficient Alignment(S, T, score) |
相应地,我们也可以使用S和T的后缀获得分数和对齐方式。
但是这种方式无法回溯对齐信息。因此,一种更聪明的方式是我们可以将S分为两半(where \(S_{\frac{m}{2}}\)is aligned to,denoted as q),分别计算对齐信息。 \[OPT(S,T) = OPT(S[1...\frac{m}{2}],T[1..q])+OPT(S[\frac{m}{2}+1..m],T[q+1..n])\]
1 | Linear Space Alignment( S, T ) |
先把S分为恰好左一半(前缀,forward)和右一半(后缀,backward),拿'OCUR'与T的总体作比较.从图中可以看出,分数一定是由S前一半的上半部分和后一半的下半部分产生。
由此,所需的空间仅为\(O(m+n)\).
Time complexity
我们可以发现,足够相似的是对角线,因此我们可以只关心条带内。 Banded DP仅需花费\(O(\alpha n)\)的时间复杂度。它为线性的算法,比原来快得多,条带外的部分我们可以不用考虑。
Local
全局比对:识别两个完整序列之间的相似性。
局部对齐:我们通常希望找到相似的段(子序列)。 局部对齐的目的是识别两个序列的相似区段。 其他区域可以被视为独立的,因此形成“随机匹配”。
通过增加额外的概率来改变递归: \[OPT(i,j) = max \left\{\begin{array}{lr}0 \\s(S_i,T_j) + OPT(i-1, j-1) \\s('-',T_j) + OPT(i, j-1) \\s(S_i,'-') + OPT(i-1, j)\end{array}\right.\] 其中0对应一个新的对齐。它与旧的对齐无关。
与从右下角回溯不同,这里从最大的项开始回溯,13为相似的终点,右下角的7为相似+不同。