資讀-進階DP
建國中學 賴昭勳
還記得DP在幹嘛ㄇ
給你一個由 P, E, C 組成的陣列,要將陣列分成三段連續的區間,且每段各派給 P, E, C三人。一個人拿到的分數是他的區間裡面跟他一樣的元素個數。求最大得分。
n≤2∗106
小小補充
DP 的順序
在做DP 的時候,必須找到一種順序,使得
一個狀態的所有轉移來源都已經被算過。
有時候,改變一下轉移順序會讓你對問題有新的觀點!
DP 的轉移方式
Top-Down: 從目前算好的狀態去更新後面的狀態
Bottom-Up: 遇到現在的狀態,看他是從哪裡轉移過來的
DP 的轉移方式:
pros and cons
Top-Down:
優點:好做,有時候會比較快(BFS)
缺點:在某些狀況下不能用 (DP 優化)
Bottom-Up:
優點:大部分時間都能使用,用數學觀點了解DP
缺點:有時候比較難寫(但其實我覺得還好)
經典問題
LIS:最長遞增子序列
給你一個序列 a,求最長的遞增子序列長度。
(子序列:選一些序列內的元素形成新的序列,這些元素在新序列的順序不變)
n≤105,ai≤109
令dp[i]代表以第i個元素結尾的LIS長度
轉移:dp[i]=maxj<i,aj<aidp[j]+1
答案:maxdp[i]
這樣只能 O(n2)?
方法一:資料結構
先對數字做值域壓縮,使得1≤ai≤n
對值域維護一個 BIT,儲存該數值大小目前最大的dp值
在 modify 的時候,把平常用BIT的加法改成取 max 的運算
原本 dp 轉移可以看成一種前綴 max,在 O(logn)取得
#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,使得vi代表長度為i的遞增子序列的最後一個數字中最小可以是多少,那可以怎麼維護?
1
5
3
4
8
2
3
5
6
1
5
3
4
8
2
3
5
6
陣列保證必然存在 「當前陣列長度」的LIS!
實作:用lower_bound
lower_bound: 回傳第一個≥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
∣a∣,∣b∣≤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
∣a∣,∣b∣≤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≤300
先想 O(n4)怎麼做?
還記得最大連續和怎麼做嗎?
結合枚舉和最大連續和的想法
i
j
把這個方向當成一個最大連續和看!O(n3)

練習題囉
開一堆怪東東
噢對然後不一定跟前面講的東西有關www
給你n∗m的地圖,上面有障礙物和寶藏位置。有兩個人各從左上角開始走到右下角要拿寶藏,他們只能往右和往下走,且一個人拿過的另一個人不會再拿到。求最多可以拿到幾個寶藏。
n,m≤100
Grazing On the Run
Bessie the cow 要吃數線上 n 株草,每一株的位置是ai,而她一開始在位置L,每秒可以向左或向右移動一單位。若她在第ti秒吃完第i株草,請找到吃完所有草的最小時間總和,也就是min(i=0∑n−1ti)
Hint:時間比較小就代表總時間少嗎?
Make It Increasing
給你一個正整數序列a,一開始每個位置都有「可以動」跟「不能動」兩種。你每次操作可以把一個可以動的數字修改為任意數。問最少需要經過幾次操作,才能使a呈嚴格遞增?(不可能的話輸出 -1)
n≤5∗105
位元DP
用二進位表示某種「有」或「沒有」的狀態
通常用在複雜度要指數的問題(n≤25)
(或是你不知道正解是什麼的喇分)
轉移:把一些 0 轉成 1 或 1 轉成 0。
答案:通常是全部選 dp[1111112] 或全部不選dp[0]
來看例題吧:
給你一個n×n 的矩陣,你必須選出n個數字,使得這些數字都在不同的行跟列上,且乘積最大。
n≤20
是不是感覺沒有好做法?
那就來枚舉吧!但是直接枚舉好像太沒效率...
觀察一下
- 假設我已經幫前 i列選好了,那麼之後再也不會用到他們。
- 每一列都一定要選一個
- 在考慮能不能選a[i][j]的時候,只要看他前面有沒有和他同一行的東西就好了,不用考慮他們的順序
來DP 吧
假設dp[bitmask]有k個 1,那麼他代表的是前k排選的位置分別為bitmask裡面 1 的 bit。
轉移:
dp[i]=max(dp[i−2j]∗a[j])
其中 i&2j=0
跟位元運算有關的語法
2x -> 1<<x
111112 -> (1<<5) - 1
bitwise and (&), or (|), xor (^)
給你一個由0, 1組成的矩陣,每次操作可以改變一個元素的值,問最少要多少操作,使得所有邊長為偶數的正方形方陣都有奇數個1
n∗m≤106
這題很讚(但很難)
圖論 x DP
DAG 有向無環圖
什麼?這也可以DP?
其實一般的DP轉移就可以把它當作一個DAG看,只要找一個好的轉移順序,絕對是可以做到的。
進階DP (資讀)
By justinlai2003
進階DP (資讀)
- 1,620