建國中學 賴昭勳
給你一個由 P, E, C 組成的陣列,要將陣列分成三段連續的區間,且每段各派給 P, E, C三人。一個人拿到的分數是他的區間裡面跟他一樣的元素個數。求最大得分。
\( n\leq 2*10^6\)
有時候,改變一下轉移順序會讓你對問題有新的觀點!
優點:好做,有時候會比較快(BFS)
缺點:在某些狀況下不能用 (DP 優化)
優點:大部分時間都能使用,用數學觀點了解DP
缺點:有時候比較難寫(但其實我覺得還好)
給你一個序列 \(a\),求最長的遞增子序列長度。
(子序列:選一些序列內的元素形成新的序列,這些元素在新序列的順序不變)
\(n \leq 10^5, a_i \leq 10^9\)
轉移:\(dp[i] = max_{j < i, a_j < a_i} dp[j] + 1\)
答案:\( \max dp[i] \)
這樣只能 \(O(n^2)\)?
在 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;
}
1
5
3
4
8
2
3
5
6
1
5
3
4
8
2
3
5
6
陣列保證必然存在 「當前陣列長度」的LIS!
#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;
}
(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]\),則它可以從三種地方轉移:
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
給你起始字串\(a\)和目標字串\(b\),你每一秒可以做三種操作:
求最少可以在幾秒內將\(a\)變成\(b\)
\(\vert a \vert, \vert b \vert \leq 3000\)
用「配對」的觀點想想看
每多一個空白就是多一秒,同位置不相同的兩個字元也要花一秒修改。
空白 -> 插入、刪除操作 (Insert, Delete)
同位置不相同的兩個字元 -> 失配 (mismatch)
|
|
---|---|
|
當前狀態 |
\(a\)的方向
\(b\)的方向
刪除
插入
配對
失配
#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嗎?
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")
\(n, m \leq 300\)
先想 \(O(n^4)\)怎麼做?
\(i\)
\(j\)
把這個方向當成一個最大連續和看!\(O(n^3)\)
開一堆怪東東
噢對然後不一定跟前面講的東西有關www
給你\(n * m\)的地圖,上面有障礙物和寶藏位置。有兩個人各從左上角開始走到右下角要拿寶藏,他們只能往右和往下走,且一個人拿過的另一個人不會再拿到。求最多可以拿到幾個寶藏。
\(n, m \leq 100\)
Bessie the cow 要吃數線上 \(n\) 株草,每一株的位置是\(a_i\),而她一開始在位置\(L\),每秒可以向左或向右移動一單位。若她在第\(t_i\)秒吃完第\(i\)株草,請找到吃完所有草的最小時間總和,也就是$$min(\sum_{i = 0}^{n -1} t_i)$$
給你一個正整數序列\(a\),一開始每個位置都有「可以動」跟「不能動」兩種。你每次操作可以把一個可以動的數字修改為任意數。問最少需要經過幾次操作,才能使\(a\)呈嚴格遞增?(不可能的話輸出 -1)
\(n \leq 5*10^5\)
(或是你不知道正解是什麼的喇分)
轉移:把一些 0 轉成 1 或 1 轉成 0。
答案:通常是全部選 \(dp[111111_2]\) 或全部不選\(dp[0]\)
給你一個\(n \times n\) 的矩陣,你必須選出\(n\)個數字,使得這些數字都在不同的行跟列上,且乘積最大。
\(n \leq 20\)
是不是感覺沒有好做法?
那就來枚舉吧!但是直接枚舉好像太沒效率...
假設\(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\)
其實一般的DP轉移就可以把它當作一個DAG看,只要找一個好的轉移順序,絕對是可以做到的。