# [資演] 動態規劃 (II): 最長共同子序列
###### tags: `資演`
這邊我們介紹另一個動態規劃的經典問題:最長共同子序列(Longest Common Subsequence, LCS)。
## 問題
最長共同子序列問題指的是在兩個array裡面找出所有共同的子序列中**最長的子序列的長度**。子序列和subarray不同的地方在於:子序列不需要在原本的array中占用連續的位置,只需要保持前後的相對順序,中間可以夾雜別的元素。
舉例來說,我們現在有兩個array $S_1$ 和 $S_2$:
$$\begin{split}
S_1 &= [2,5,7,9,4,1,3]\\
S_2 &= [3,5,4,5,7, 3]
\end{split}
$$則他們的LCS是:
$$
\text{LCS} (S_1, S_2) = [5,4,3]
$$
## 解法
我們知道動態規劃有兩個要素:
* 最佳子結構(optimal substructure)
原問題可以分解成更小,更簡單的「子問題」,而這個子問題可以進一步分解成更小的子問題。
* 重疊子問題(overlapping subproblem)
子問題的解是可以重複使用的,也就是說,較高階的子問題(在遞迴樹上較接近root的問題)通常會重複使用低階子問題(在遞迴樹上較接近葉子的問題)的解。
有了這兩個要素,就代表原問題可以使用動態規劃來對時間複雜度來進行優化。
那要怎麼看出這個問題滿足這些要素呢?首先我們必須先使用divide and conquer來找出一個遞迴暴力解,所以最重要的是要如何對這個原問題進行**分割**。
假設有二個array為 $S_1$ 和 $S_2$,其**最長共同子序列**(LCS)表示為 $\text{LCS} (S_1, S_2)$。$S_1$ 和 $S_2$ 的最後一個元素分別以 $e_1$ 與 $e_2$ 表示,剩餘部分以 $S'_1$ 與 $S'_2$ 表示,也就是說:
$$\begin{split}
S_1&=S'_1+e_1\\
S_2&=S'_2+e_2
\end{split}
$$根據上面的表示,我們可以將 $S_1$ 與 $S_2$ 的LCS分為以下幾種情形:
* LCS 包含 $e_1$ 但不包含 $e_2$
此種情形 $e_2$ 沒有用處,若要找到 LCS 只需要從 $S'_2$ 去找:
$$
\text{LCS} (S_1, S_2) = \text{LCS} (S_1, S'_2)
$$
* LCS 包含 $e_2$ 但不包含 $e_1$
此種情形 $e_1$ 沒有用處,若要找到 LCS 只需要從 $S'_1$ 去找。
$$
\text{LCS} (S_1, S_2) = \text{LCS} (S'_1, S_2)
$$
* LCS 不包含 $e_1$ 也不包含 $e_2$
此種情形 $e_1$ 和 $e_2$ 都沒有用處,若要找到 LCS 只需要從 $S'_1$ 和 $S'_2$ 去找:
$$
\text{LCS} (S_1, S_2) = \text{LCS} (S'_1, S'_2)
$$
* LCS 包含 $e_1$ 也包含 $e_2$
此種情形 LCS 同時包含 $e_1$ 和 $e_2$,且 $e_1$ 和 $e_2$ 分別為2個array的最後一個元素,並且 $e_1=e_2$,也就是說,LCS 的最後一個元素必定為 $e_1$ (也就是 $e_2$):
$$
\text{LCS} (S_1, S_2) = \text{LCS} (S'_1, S'_2) + 1
$$
另外我們也考慮到一個base case:當 $S_1$ 或 $S_2$ 是空的array時,LCS也是空的。
這裡我們可以建立一個表格`dp`,其中`dp[i][j]`這個元素代表的意思是`s1[0:i]`和`s2[0:j]`這兩個 subarray 的 LCS 長度。
根據以上邏輯,我們可以將演算法寫成Python code如下:
```python=
def lcs(s1, s2):
n1 = len(s1)
n2 = len(s2)
dp = [[None] * (n2 + 1) for i in range(n1 + 1)]
for i in range(n1 + 1):
for j in range(n2 + 1):
if i == 0 or j == 0 :
dp[i][j] = 0
elif s1[i - 1] == s2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
return dp[m][n]
```