# 【6-8】DP
動態規劃(Dynamic Programming,簡稱 DP)聽起來像「邊走邊看」很隨興的一種演算法,但其實是一種非常有系統性作法:**找出問題中的重複子問題(overlapping subproblems)與最優子結構(optimal substructure),把子問題的解保存起來,避免重複計算。** 換句話說,DP 常常是「以空間換取時間」的策略。
題目:給定一個正整數 $k,計算 $2^k$,從 $1$ 到 $k$ 的所有結果
### 解法 A:每次都從頭乘
```cpp
for (int i = 1; i <= N; ++i) {
long long n = 1;
for (int j = i; j > 0; --j) n = n * 2;
cout << "2^" << i << " = " << n << '\n';
}
```
* **時間複雜度**:$O(n^2)$
* **空間複雜度**:$O(1)$
### 解法 B:用前一個計算結果乘
```cpp
vector<long long> a(N+1);
a[0] = 1;
for (int i = 1; i <= N; ++i) {
a[i] = a[i-1] * 2;
cout << "2^" << i << " = " << a[i] << '\n';
}
```
* **時間複雜度**:$O(n)$
* **空間複雜度**:$O(n)$
解法 B 就是使用了 DP 的技巧,而在使用 DP 時,通常有三個要素:
* **狀態定義**:定義陣列中紀錄的內容代表什麼
* **Base case**:給出最小子問題的解
```cpp
a[0] = 1
```
* **狀態轉移**:定義如何由較小子問題組出較大問題的解
```cpp
a[i] = a[i-1] * 2
```
實作上,又有兩種常見方法,接下來的例題會盡量提供兩種解法給各位讀者參考:
* **Top-down + memoization**:遞迴求解,遇到新子問題就計算並存結果。
* **Bottom-up / tabulation**:從最小子問題開始按順序填表,直到得到答案。
---
## 0/1 背包(0/1 knapsack)
**問題**:有 `n` 件物品,第 `i` 件重量 `w[i]`、價值 `v[i]`,背包容量 `W`。每件物品最多選 1 次,不可分割,求最大總價值。
初次遇到這種問題,我們先土法煉鋼一下,假設題目如下:
背包容量W=8
|編號|重量|價值|
|-|-|-|
|1|2|1|
|2|3|2|
|3|4|5|
|4|5|6|
如果使用窮舉法,可以找到以下可能:
```
1+2+3+4:重量14 價值14 (超重)
1+2+3 :重量9 價值8 (超重)
1+2+4 :重量10 價值9 (超重)
1+3+4 :重量11 價值12 (超重)
2+3+4 :重量12 價值13 (超重)
1+2 :重量5 價值3
1+3 :重量6 價值6
1+4 :重量7 價值7
2+3 :重量7 價值7
2+4 :重量8 價值8
3+4 :重量9 價值11 (超重)
1 :重量2 價值1
2 :重量3 價值2
3 :重量4 價值5
4 :重量5 價值6
```
答案為 8,但窮舉法的時間複雜度是 $O(2^n)$,當物品種類變多,效率會變很差。
---
窮舉法之所以不方便解決這個問題,是因為物品過多,導致組合數太多。
既然如此,為何我們不考慮先討論兩個物品就好呢?找出兩個問題的解後,再放入第三個物品,一次增加一種,並且加入一個價值、重量皆為 0 的物品,方便討論。
### 狀態定義
`dp[i][j]`:考慮前 `i` 件物品,且背包容量為 `j` 時能達到的最大價值
### Base case
`dp[0][j] = 0`(未考慮任何物品,價值為 0)
`dp[i][0] = 0`(容量為 0,價值為 0)
### 狀態轉移
考慮第 i 種物品,最大重量為 j 的背包,比較【放 / 不放】:
* 【不放】不考慮此物品時最佳解(`dp[i-1][j]`)
* 【放】不考慮此物品且放得下此物品的最佳解 + 本物品價值(`dp[i-1][j-w[i]]+v[i]`)
得到狀態轉移方程式:
```cpp
dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i])
```
在遍歷時要注意,j必須大於等於 v[i],否則會越界(跑到陣列外)。若j小於 v[i],由於無法將物品放入背包,故最佳解必為【不放】。
---
### 圖解程式
※ 紅色那格為 dp[0][0]
| 標號 | 價值 | 重量 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 0 | 0 | 0 | <font color="#f00">**0**</font> | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 1 | 2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 2 | 2 | 3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 3 | 5 | 4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 4 | 6 | 5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
我們先對編號 1 的物體進行討論。由於j必須大於等於 v[i],所以從重量上限 2 開始討論。考慮【放 / 不放】:
* 【不放】dp[0][2] = <font color="#F7A004">**0**</font>
* 【放】dp[0][2 – w[1]] + v[1] = dp[0][2 - 2] + v[1] = <font color="#00A600">**0**</font> + <font color="#0080FF">**1**</font> = <font color="#f00">**1**</font>
顯然 <font color="#f00">**1**</font> > <font color="#F7A004">**0**</font>,所以dp[1][2]= <font color="#f00">**1**</font>
| 標號 | 價值 | 重量 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 0 | 0 | 0 | <font color="#00A600">**0**</font> | 0 | <font color="#F7A004">**0**</font> | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | <font color="#0080FF">**1**</font> | 2 | 0 | 0 | <font color="#f00">**1**</font> | 0 | 0 | 0 | 0 | 0 | 0 |
| 2 | 2 | 3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 3 | 5 | 4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 4 | 6 | 5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
顯然對於重量上限 3~8 最佳解都會是 1。
| 標號 | 價值 | 重量 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 1 | 2 | 0 | 0 | 1 | <font color="#f00">**1**</font> | <font color="#f00">**1**</font> | <font color="#f00">**1**</font> | <font color="#f00">**1**</font> | <font color="#f00">**1**</font> | <font color="#f00">**1**</font> |
| 2 | 2 | 3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 3 | 5 | 4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 4 | 6 | 5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
接著加入編號 2 的物體進行討論。由於j必須大於等於 v[i],所以最佳解必為【不放】。
| 標號 | 價值 | 重量 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 1 | 2 | <font color="#F7A004">**0**</font> | <font color="#F7A004">**0**</font> | <font color="#F7A004">**1**</font> | 1 | 1 | 1 | 1 | 1 | 1 |
| 2 | 2 | 3 | <font color="#f00">**0**</font> | <font color="#f00">**0**</font> | <font color="#f00">**1**</font> | 0 | 0 | 0 | 0 | 0 | 0 |
| 3 | 5 | 4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 4 | 6 | 5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
重量上限 3 時,考慮【放 / 不放】:
* 【不放】dp[1][3] = <font color="#F7A004">**1**</font>
* 【放】dp[1][3 – w[2] + v[2] = dp[1][3 - 3] + v[2] = <font color="#00A600">**0**</font> + <font color="#0080FF">**2**</font> = <font color="#f00">**2**</font>
顯然 <font color="#f00">**2**</font> > <font color="#F7A004">**1**</font>,所以dp[1][2]= <font color="#f00">**2**</font>
| 標號 | 價值 | 重量 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 1 | 2 | <font color="#00A600">**0**</font> | 0 | 1 | <font color="#F7A004">**1**</font> | 1 | 1 | 1 | 1 | 1 |
| 2 | <font color="#0080FF">**2**</font> | 3 | 0 | 0 | 1 | <font color="#f00">**2**</font> | 0 | 0 | 0 | 0 | 0 |
| 3 | 5 | 4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 4 | 6 | 5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
接著重量上限 5 的時候,考慮【放 / 不放】:
* 【不放】dp[1][5] = <font color="#F7A004">**1**</font>
* 【放】dp[1][5 – w[2] + v[2] = dp[1][5 - 3] + v[2] = <font color="#00A600">**1**</font> + <font color="#0080FF">**2**</font> = <font color="#f00">**3**</font>
顯然 <font color="#f00">**3**</font> > <font color="#F7A004">**1**</font>,所以dp[1][2]= <font color="#f00">**3**</font>
| 標號 | 價值 | 重量 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 1 | 2 | 0 | 0 | <font color="#00A600">**1**</font> | 1 | 1 | <font color="#F7A004">**1**</font> | 1 | 1 | 1 |
| 2 | <font color="#0080FF">**2**</font> | 3 | 0 | 0 | 1 | 2 | 2 | <font color="#f00">**3**</font> | 0 | 0 | 0 |
| 3 | 5 | 4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 4 | 6 | 5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
依此類推,完成表格,最後我們可以看到,背包重量上限 8 的情況下,考慮所有物品放入情況,最大價值為 <font color="#f00">**8**</font> 。
| 標號 | 價值 | 重量 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 1 | 2 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| 2 | 2 | 3 | 0 | 0 | 1 | 2 | 2 | 3 | 3 | 3 | 3 |
| 3 | 5 | 4 | 0 | 0 | 1 | 2 | 5 | 5 | 6 | 7 | 7 |
| 4 | 6 | 5 | 0 | 0 | 1 | 2 | 5 | 6 | 6 | 7 | <font color="#f00">**8**</font> |
---
### C++範例
```cpp=
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, W;
cin >> n >> W;
vector<int> w(n+1), v(n+1);
for (int i = 1; i <= n; i++) cin >> w[i] >> v[i];
vector<vector<int>> dp(n+1, vector<int>(W+1, 0));
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= W; j++) {
dp[i][j] = dp[i-1][j];
if (j >= w[i])
dp[i][j] = max(dp[i][j], dp[i-1][j - w[i]] + v[i]);
}
}
cout << dp[n][W];
}
```
## LCS(Longest Common Subsequence)
**問題**:LCS(最長共同子序列)的定義為出現於每一個序列、而且是最長的子序列,給定兩字串 `s1`、`s2`,試求 `s1` 和 `s2` 的 LCS 長度與 LCS 為何?
我們先看個例子確保了解 LCS 的概念:
```
longest
love
```
從頭開始比較,`l` 相同,`o` 相同,下一個是 `e`,因此兩者的 LCS 為 `loe`
再看一個例子:
```
abcdefghijklmnzyxwvutsrqpo
opqrstuvwxyzabcdefghijklmn
```
這時候就頭昏眼花了吧,有沒有什麼方法可以讓我們有系統性的找出最長共同子序列呢?
---
回到最一開始的舉例,我們來從中觀察看看有沒有可以轉化成 DP 的可能性:
```
longest
love
```
既然第一個字母 `l` 相同,那我其實只需要找到 `ongest` 和 `ove` 的最長共同子序列,再補上 `l` 就是答案了。
```
ongest
ove
```
既然第一個字母 `o` 相同,那我其實只需要找到 `ngest` 和 `ve` 的最長共同子序列,再補上 `o` 就是答案了。
```
ngest
ve
```
`v` 和 `ngest` 都沒有相同的字母,這時候要把 `e` 還是 `v` 刪掉,好像都不對,兩種情況都應該考慮才對,但總之,LCS 的長度並沒有改變。
---
不過要是呼叫遞迴,複雜度就會變成 $O(2^n)$ (討論兩種情況),顯然不是我們樂見的,怎麼辦呢?
其實剛才處理的過程,我們觀察到一個很有用的東西:
* 相同:LCS 長度 +1,可以移除該相同字元
* 不同:不影響 LCS 長度
那我們不妨用雙重 `for` 來找出兩字串相同的字元,並在找到以後記錄下來 LCS 長度
### 狀態定義
`dp[i][j]`:字串 `s1` 的前 `i` 個字元與字串 `s2` 的前 `j` 個字元的 LCS 長度。
### Base case
`dp[0][j] = 0`(未考慮任何字元,LCS 必為空字串,長度為 0)
`dp[i][0] = 0`(未考慮任何字元,LCS 必為空字串,長度為 0)
### 狀態轉移
```cpp
if(s1[i] == s2[j]){
dp[i][j] = dp[i-1][j-1] + 1;
}
else{
dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
```
---
### 圖解程式
| | | l | o | n | g | e | s | t |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| **l** | 0 | | | | | | | |
| **o** | 0 | | | | | | | |
| **v** | 0 | | | | | | | |
| **e** | 0 | | | | | | | |
從左上開始,先比較 l 和 l 是否相同,發現相同,所以從左上那格+1填入此格。
| | | <font color="#f00">l</font> | o | n | g | e | s | t |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| | <font color="#1936C9">**0**</font> | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| <font color="#f00">**l**</font> | 0 | <font color="#AC19C9">**1**</font> | | | | | | |
| **o** | 0 | | | | | | | |
| **v** | 0 | | | | | | | |
| **e** | 0 | | | | | | | |
接著比較 o 和 l 是否相同,發現不同,所以從上方和左方挑選最大的填入此格。
| | | l | <font color="#f00">o</font> | n | g | e | s | t |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| | 0 | 0 | <font color="#1936C9">**0**</font> | 0 | 0 | 0 | 0 | 0 |
| <font color="#f00">**l**</font> | 0 | <font color="#1936C9">**1**</font> | <font color="#AC19C9">**1**</font> | | | | | |
| **o** | 0 | | | | | | | |
| **v** | 0 | | | | | | | |
| **e** | 0 | | | | | | | |
依序遍歷完後,會得到下方表格:
| | | l | o | n | g | e | s | t |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| **l** | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| **o** | 0 | 1 | 2 | 2 | 2 | 2 | 2 | 2 |
| **v** | 0 | 1 | 2 | 2 | 2 | 2 | 2 | 2 |
| **e** | 0 | 1 | 2 | 2 | 2 | 3 | 3 | 3 |
LCS 長度為 3
可以用這個 [LCS建表練習](https://alchemist-al.com/algorithms/longest-common-subsequence) 檢測看看自己有沒有理解喔!
---
此時若題目只詢問 LCS 長度,就只需要輸出最後一格就好了,但想知道這個 LCS 具體是什麼也難不倒我們,只需要把剛才遍歷的狀態轉移倒轉就可以了:
* 優先往大的地方走(只能往左和往上)
* 發現下一格數字改變,記錄字母於陣列中,並且跳到左上方的格子
| | | l | o | n | g | <font color="#f00">e</font> | s | t |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| **l** | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| **o** | 0 | 1 | 2 | 2 | 2 | 2 | 2 | 2 |
| **v** | 0 | 1 | 2 | 2 | <font color="#F7A004">2</font> | 2 | 2 | 2 |
| <font color="#f00">**e**</font> | 0 | 1 | 2 | 2 | <font color="#1936C9">**2**</font> | <font color="#AC19C9">**3**</font> | 3 | 3 |
陣列:`e`
優先往大的地方走(但是只能往左和往上)。
| | | l | o | n | g | e | s | t |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| **l** | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| **o** | 0 | 1 | <font color="#F7A004">2</font> | 2 | 2 | 2 | 2 | 2 |
| **v** | 0 | 1 | <font color="#AC19C9">**2**</font> | 2 | 2 | 2 | 2 | 2 |
| **e** | 0 | 1 | 2 | 2 | 2 | 3 | 3 | 3 |
| | | l | <font color="#f00">o</font> | n | g | e | s | t |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| **l** | 0 | <font color="#F7A004">1</font> | 1 | 1 | 1 | 1 | 1 | 1 |
| <font color="#f00">**o**</font> | 0 | <font color="#1936C9">**1**</font> | <font color="#AC19C9">**2**</font> | 2 | 2 | 2 | 2 | 2 |
| **v** | 0 | 1 | 2 | 2 | 2 | 2 | 2 | 2 |
| **e** | 0 | 1 | 2 | 2 | 2 | 3 | 3 | 3 |
陣列:`e,o`
| | | <font color="#f00">l</font> | o | n | g | e | s | t |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| | <font color="#F7A004">0</font> | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| <font color="#f00">**l**</font> | <font color="#1936C9">**0**</font> | <font color="#AC19C9">**1**</font> | 1 | 1 | 1 | 1 | 1 | 1 |
| **o** | 0 | 1 | 2 | 2 | 2 | 2 | 2 | 2 |
| **v** | 0 | 1 | 2 | 2 | 2 | 2 | 2 | 2 |
| **e** | 0 | 1 | 2 | 2 | 2 | 3 | 3 | 3 |
陣列:`e,o,l`
最後反向輸出陣列即可得到最長共同子序列 `loe` 。
---
### C++範例
```cpp=
#include <bits/stdc++.h>
using namespace std;
int main() {
string A, B;
cin >> A >> B;
int n = A.size(), m = B.size();
vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
// 填 DP 表
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (A[i - 1] == B[j - 1])
dp[i][j] = dp[i - 1][j - 1] + 1;
else
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
// 反向追溯 LCS 字串
string lcs = "";
int i = n, j = m;
while (i > 0 && j > 0) {
if (A[i - 1] == B[j - 1]) {
lcs += A[i - 1];
i--; j--;
} else if (dp[i - 1][j] >= dp[i][j - 1]) {
i--;
} else {
j--;
}
}
reverse(lcs.begin(), lcs.end());
cout << "LCS length = " << dp[n][m] << "\n";
cout << "LCS string = " << lcs;
}
```
---
## LIS(Longest Increasing Subsequence)
**問題**:LIS(最長遞增子序列)的定義為所有子序列中,遞增的、最長的子序列。今給定一數列 `S`,試問其 LIS 長度與 LIS 序列為何?
```
1 4 2 3
```
我們可以把所有子序列找出來:
```
1 4 2 3
1 4 2
1 4 3
1 2 3
1 4
1 2
1 3
1
4 2 3
4 2
4 3
4
2 3
2
3
```
其中不屬於遞增(由小到大)的我們將其移除,留下遞增子序列:
```
1 2 3
1 4
1 2
1 3
1
2 3
2
3
```
這之中最長的為 1 2 3,故最長遞增子序列為 1 2 3,長度為3。
但窮舉出每一種可能的複雜度是 $O(2^n)$,顯然不是一個好的方法。
---
我們根據 LIS 的規則,不難發現其要求有兩點:
* 前後順序不變
* 數值遞增
那麼,有了 LCS 的經驗,我們再一次透過雙重 `for` 迴圈,但這次改比較兩個數字的大小關係,要是後者大於前者就代表可以加入 LIS 中(但不保證是最優解),反之就不可能放入 LIS。
### 狀態定義
`dp[i]`:數列 `S` 的前 `i` 個數值的 LIS 長度。
### Base case
`dp[i] = 1`(所有數字都可以自己成為 LIS)
### 狀態轉移
```cpp
// i<j
if(S[i]<S[j]){
dp[j]=max(dp[i]+1,dp[j]);
}
else{
dp[j]=dp[j-1];
}
```
---
### 圖解程式
4 > 1,`max`(1 + 1 , 1)= 2。
| <font color="#1936C9">1</font> | <font color="#f00">4</font> | 2 | 3 |
|---- | ---- | ---- | ---- |
| <font color="#F7A004">**1**</font> | <font color="#AC19C9">**2**</font> | 1 | 1 |
2 > 1,`max`(1 + 1 , 1)= 2。
| <font color="#1936C9">1</font> | 4 | <font color="#f00">2</font> | 3 |
|---- | ---- | ---- | ---- |
| <font color="#F7A004">**1**</font> | 2 | <font color="#AC19C9">**2**</font> | 1 |
2 < 4,不變。
| 1 | <font color="#1936C9">4</font> | <font color="#f00">2</font> | 3 |
|---- | ---- | ---- | ---- |
| 1 | <font color="#F7A004">**2**</font> | <font color="#AC19C9">**2**</font> | 1 |
3 > 1,`max`(1 + 1 , 1)= 2。
| <font color="#1936C9">1</font> | 4 | 2 | <font color="#f00">3</font> |
|---- | ---- | ---- | ---- |
| <font color="#F7A004">**1**</font> | 2 | 2 | <font color="#AC19C9">**2**</font> |
3 < 4,不變。
| 1 | <font color="#1936C9">4</font> | 2 | <font color="#f00">3</font> |
|---- | ---- | ---- | ---- |
| 1 | <font color="#F7A004">**2**</font> | 2 | <font color="#AC19C9">**2**</font> |
3 > 2,`max`(2 + 1 , 2)= 3。
| 1 | 4 | <font color="#1936C9">2</font> | <font color="#f00">3</font> |
|---- | ---- | ---- | ---- |
| 1 | 2 | <font color="#F7A004">**2**</font> | <font color="#AC19C9">**3**</font> |
故 LIS 長度為 3
可以用這個 [LIS操作練習](https://alchemist-al.com/algorithms/longest-increasing-subsequence) 檢測看看自己有沒有理解喔!
---
若要找出此子序列,我們可以依照動態轉移的規則進行反向遍歷。
* 紀錄第一個此長度的數字
* 長度為 1 時結束
首次出現長度為 3,紀錄 3。
| 1 | 4 | 2 | <font color="#f00">3</font> |
|---- | ---- | ---- | ---- |
| 1 | 2 | 2 | <font color="#AC19C9">**3**</font> |
首次出現長度為 2,紀錄 2。
| 1 | 4 | <font color="#f00">2</font> | 3 |
|---- | ---- | ---- | ---- |
| 1 | 2 | <font color="#AC19C9">**2**</font> | 3 |
長度 2 並非首次出現,跳過不紀錄。
| 1 | <font color="#f00">4</font> | 2 | 3 |
|---- | ---- | ---- | ---- |
| 1 | <font color="#AC19C9">**2**</font> | 2 | 3 |
首次出現長度為 1,紀錄 1。
| <font color="#f00">1</font> | 4 | 2 | 3 |
|---- | ---- | ---- | ---- |
| <font color="#AC19C9">**1**</font> | 2 | 2 | 3 |
---
### C++範例
```cpp=
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; i++) cin >> a[i];
vector<int> dp(n, 1), pre(n, -1); // pre[i] 記錄 dp[i] 來源位置
int ans = 1, pos = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < i; j++) {
if (a[j] < a[i] && dp[j] + 1 > dp[i]) {
dp[i] = dp[j] + 1;
pre[i] = j;
}
}
if (dp[i] > ans) {
ans = dp[i];
pos = i;
}
}
// 反向尋找 LIS 序列
vector<int> lis;
for (int i = pos; i != -1; i = pre[i])
lis.push_back(a[i]);
reverse(lis.begin(), lis.end());
cout << "LIS length = " << ans << "\n";
cout << "LIS sequence = ";
for (int x : lis) cout << x << " ";
}
```
---
### 補充(只求LIS長度)
若是題目只求LIS長度,我們可以用偷懶一點的方式:嘗試找到 LIS 每個位置中可以擺放的最小值。(複雜度:$nlogn$)
```cpp=
#include<bits/stdc++.h>
using namespace std;
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
int n;
cin >> n;
vector<int> a(n); // 原序列
for(int i=0;i<n;i++){
cin >> a[i];
}
vector<int> b; // LIS每個位置的最小值
for(int i=0;i<n;i++){
if(b.empty() or b.back()<a[i]){ // 空序列or現在這個數比LIS最後一位還大
b.push_back(a[i]); // 插入到LIS最後方
}
else{
*lower_bound(b.begin(),b.end(),a[i])=a[i];
// 使用二分搜找到第一個大於a[i]的數字,將其替換成更小的a[i]。
}
}
cout << b.size(); // 輸出LIS長度
return 0;
}
```
---
聯絡方式:codecodefunny@gmail.com
最後編修時間:2025/10/19 子柚筆