資讀-進階DP
建國中學 賴昭勳
還記得DP在幹嘛ㄇ
給你一個由 P, E, C 組成的陣列,要將陣列分成三段連續的區間,且每段各派給 P, E, C三人。一個人拿到的分數是他的區間裡面跟他一樣的元素個數。求最大得分。
\( n\leq 2*10^6\)
小小補充
DP 的順序
在做DP 的時候,必須找到一種順序,使得
一個狀態的所有轉移來源都已經被算過。
有時候,改變一下轉移順序會讓你對問題有新的觀點!
DP 的轉移方式
Top-Down: 從目前算好的狀態去更新後面的狀態
Bottom-Up: 遇到現在的狀態,看他是從哪裡轉移過來的
DP 的轉移方式:
pros and cons
Top-Down:
優點:好做,有時候會比較快(BFS)
缺點:在某些狀況下不能用 (DP 優化)
Bottom-Up:
優點:大部分時間都能使用,用數學觀點了解DP
缺點:有時候比較難寫(但其實我覺得還好)
經典問題
LIS:最長遞增子序列
給你一個序列 \(a\),求最長的遞增子序列長度。
(子序列:選一些序列內的元素形成新的序列,這些元素在新序列的順序不變)
\(n \leq 10^5, a_i \leq 10^9\)
令\(dp[i]\)代表以第\(i\)個元素結尾的LIS長度
轉移:\(dp[i] = max_{j < i, a_j < a_i} dp[j] + 1\)
答案:\( \max dp[i] \)
這樣只能 \(O(n^2)\)?
方法一:資料結構
先對數字做值域壓縮,使得\(1 \leq a_i \leq n\)
對值域維護一個 BIT,儲存該數值大小目前最大的\(dp\)值
在 modify 的時候,把平常用BIT的加法改成取 max 的運算
原本 dp 轉移可以看成一種前綴 max,在 \(O(\log n)\)取得
#include <iostream>
#define maxn 100005
using namespace std;
int bit[maxn], a[maxn];
void modify(int ind, int val) {
for (;ind < maxn;ind += ind & (-ind)) bit[ind] = max(bit[ind], val);
}
int query(int ind) {
int ret = 0;
for (;ind > 0;ind -= ind & (-ind)) ret = max(ret, bit[ind]);
return ret;
}
int main() {
int n;
cin >> n;
for (int i = 0;i < n;i++) cin >> a[i];
//已經離散化了
int ans = 0;
for (int i = 0;i < n;i++) {
int dp = query(a[i] - 1);
ans = max(ans, dp);
modify(a[i], dp);
}
cout << ans << endl;
}
方法二:換個觀點
假設我有一個陣列\(v\),使得\(v_i\)代表長度為\(i\)的遞增子序列的最後一個數字中最小可以是多少,那可以怎麼維護?
1
5
3
4
8
2
3
5
6
1
5
3
4
8
2
3
5
6
陣列保證必然存在 「當前陣列長度」的LIS!
實作:用lower_bound
lower_bound: 回傳第一個\(\geq x\) 的數字的位置
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
int main() {
ios_base::sync_with_stdio(0);cin.tie(0);
int n;
cin >> n;
int a[n];
for (int i = 0;i < n;i++) cin >> a[i];
vector<int> v;
for (int i = 0;i < n;i++) {
int ind = lower_bound(v.begin(), v.end(), a[i]) - v.begin();
if (ind == v.size()) {
v.push_back(a[i]);
} else {
v[ind] = a[i];
}
}
cout << v.size() << endl;
}
LCS: 最長共同子序列
(Longest Common Subsequence)
給你兩個序列\(a, b\),在\(a, b\)中分別找出一個長度為\(x\)的子序列 \(a', b' \),使得兩個子序列相同。
請找出符合上述條件中最大的\(x\)
\(\vert a \vert, \vert b \vert \leq 3000\)
可以這樣想這題
在兩個字串中間放入任意多個「空白」字元,使得字串之間盡量多個字元對應到。(最佳配對)
ex. abaabcc 和 badcbcac
a | b | a | a | - | b | c | - | c |
---|---|---|---|---|---|---|---|---|
- | b | a | d | c | b | c | a | c |
假設位置相同、字元一樣得 1 分,求得分最大值
那這題的「重複子結構」是什麼?
考慮已經做完了\(a\)的前\(i\)個字元,做完\(b\)的前\(j\)個字元的最佳配對。令這個狀態的最大得分為\(dp[i][j]\),則它可以從三種地方轉移:
- 從\(dp[i][j - 1]\) 新增了 \(b[j]\)並在\(a\)填上一個空白
- 從\(dp[i - 1][j]\) 新增了 \(a[i]\)並在\(b\)填上一個空白
- 從\(dp[i - 1][j - 1]\) 把\(a[i], b[j]\)配對
a | b | a | a | b | c | c | |
---|---|---|---|---|---|---|---|
b | 0 | 1 | 1 | 1 | |||
a | 1 | ||||||
d | |||||||
c | |||||||
b | |||||||
c | |||||||
a | |||||||
c |
1
2
2
1
1
2
2
Edit Distance: 編輯距離
給你起始字串\(a\)和目標字串\(b\),你每一秒可以做三種操作:
- 選擇\(a\)的任意一個位置插入任意字元
- 刪除\(a\)的一個字元
- 將\(a\)的某一字元替換
求最少可以在幾秒內將\(a\)變成\(b\)
\(\vert a \vert, \vert b \vert \leq 3000\)
跟 LCS 是不是有點像?
用「配對」的觀點想想看
每多一個空白就是多一秒,同位置不相同的兩個字元也要花一秒修改。
空白 -> 插入、刪除操作 (Insert, Delete)
同位置不相同的兩個字元 -> 失配 (mismatch)
DP 方法相同!
|
|
---|---|
|
當前狀態 |
\(a\)的方向
\(b\)的方向
刪除
插入
配對
失配
Where your code
#include <iostream>
#include <algorithm>
#define ll long long
using namespace std;
int main() {
string a, b;
getline(cin, a);
getline(cin, b);
int dp[2][1005 + 1];
for (int i = 0; i < b.size() + 1;i++) {
dp[0][i] = i;
dp[1][i] = i;
}
for (int i = 1; i <= a.size();i++) {
dp[1][0] = dp[0][0] + 1;
for (int j =1; j <= b.size();j++) {
if (a[i - 1] == b[j - 1]) {
dp[1][j] = dp[0][j - 1];
} else {
dp[1][j] = min(min(dp[0][j], dp[1][j - 1]), dp[0][j - 1]) + 1;
}
}
for (int j = 0; j < b.size() + 1; j++) dp[0][j] = dp[1][j];
}
cout << dp[1][b.size()] << endl;
}
二維DP 的空間壓縮:滾動!
你有聽過滾動DP嗎?
假設這一排的狀態只有用到
這一排跟上一排的話...
Code Time!
https://tioj.ck.tp.edu.tw/problems/1175
https://tioj.ck.tp.edu.tw/problems/1385
https://tioj.ck.tp.edu.tw/problems/2010
(要做請搜尋 "Hirschberg's Algorithm")
如果我們要輸出一組解怎麼辦?
以lower_bound 為例
回溯:紀錄目前的 \(dp\)位置從哪邊轉移過來
經典問題 Part 2
最大二維子矩陣
給你一個二維整數陣列\(a[n][m]\),選擇一個長方形子矩陣,使其總和最大
\(n, m \leq 300\)
先想 \(O(n^4)\)怎麼做?
還記得最大連續和怎麼做嗎?
結合枚舉和最大連續和的想法
\(i\)
\(j\)
把這個方向當成一個最大連續和看!\(O(n^3)\)
練習題囉
開一堆怪東東
噢對然後不一定跟前面講的東西有關www
給你\(n * m\)的地圖,上面有障礙物和寶藏位置。有兩個人各從左上角開始走到右下角要拿寶藏,他們只能往右和往下走,且一個人拿過的另一個人不會再拿到。求最多可以拿到幾個寶藏。
\(n, m \leq 100\)
Grazing On the Run
Bessie the cow 要吃數線上 \(n\) 株草,每一株的位置是\(a_i\),而她一開始在位置\(L\),每秒可以向左或向右移動一單位。若她在第\(t_i\)秒吃完第\(i\)株草,請找到吃完所有草的最小時間總和,也就是$$min(\sum_{i = 0}^{n -1} t_i)$$
Hint:時間比較小就代表總時間少嗎?
Make It Increasing
給你一個正整數序列\(a\),一開始每個位置都有「可以動」跟「不能動」兩種。你每次操作可以把一個可以動的數字修改為任意數。問最少需要經過幾次操作,才能使\(a\)呈嚴格遞增?(不可能的話輸出 -1)
\(n \leq 5*10^5\)
位元DP
用二進位表示某種「有」或「沒有」的狀態
通常用在複雜度要指數的問題(\(n \leq 25\))
(或是你不知道正解是什麼的喇分)
轉移:把一些 0 轉成 1 或 1 轉成 0。
答案:通常是全部選 \(dp[111111_2]\) 或全部不選\(dp[0]\)
來看例題吧:
給你一個\(n \times n\) 的矩陣,你必須選出\(n\)個數字,使得這些數字都在不同的行跟列上,且乘積最大。
\(n \leq 20\)
是不是感覺沒有好做法?
那就來枚舉吧!但是直接枚舉好像太沒效率...
觀察一下
- 假設我已經幫前 \(i\)列選好了,那麼之後再也不會用到他們。
- 每一列都一定要選一個
- 在考慮能不能選\(a[i][j]\)的時候,只要看他前面有沒有和他同一行的東西就好了,不用考慮他們的順序
來DP 吧
假設\(dp[bitmask]\)有\(k\)個 1,那麼他代表的是前\(k\)排選的位置分別為\(bitmask\)裡面 1 的 bit。
轉移:
\(dp[i] = \max (dp[i - 2^j] * a[j]) \)
其中 \(i \& 2^j \neq 0\)
跟位元運算有關的語法
\(2^x\) -> 1<<x
\(11111_2\) -> (1<<5) - 1
bitwise and (&), or (|), xor (^)
給你一個由0, 1組成的矩陣,每次操作可以改變一個元素的值,問最少要多少操作,使得所有邊長為偶數的正方形方陣都有奇數個1
\(n * m \leq 10^6\)
這題很讚(但很難)
圖論 x DP
DAG 有向無環圖
什麼?這也可以DP?
其實一般的DP轉移就可以把它當作一個DAG看,只要找一個好的轉移順序,絕對是可以做到的。
進階DP (資讀)
By justinlai2003
進階DP (資讀)
- 1,564