9/20 校隊培訓
becaido
講師介紹
經歷:
因為想學習 greedy/dc/dp
所以來當 greedy/dc/dp 講師
貪心
有的時候直接貪心可能會出事
如果可以反悔的話...
有一個長度 \(n\leq 5\times 10^5\) 的字串,字元可能有 ()? 三種
? 有 \(m\) 個,第 \(i\) 個 ? 要變成 ( 或 )
變成 ( 的代價是 \(L_i\)
變成 ) 的代價是 \(R_i\)
問讓這個字串變成一個合法的括號字串最少的代價
看起來很難...
不如先把所有 ? 都先變成 )
等到出事了再說
目前的總代價是 \(\sum R_i\)
從左往右掃
記錄現在有多少個 ( 沒被配對到
如果數量 \(<0\)
出事了!
必須要讓一個 ) 變成 ( 才行
要從所有可以變成 ( 的 ) 找代價最小的...
用 priority_queue 存!
遇到原本是 ? 的位置就在 pq 裡 push 進 \(L_i-R_i\)
代表從 ) 變成 ( 的代價
貪心的地方在於每次選最小的一定是最好的
證明是當遇到下一個 ?,我們把之前的決策改變不會比較好
分治
分治叫 Divide and Conquer
先把大問題拆成小問題做
然後把小的合併成大的
經典分治例子:Merge Sort
給一個長度為 \(n\leq 10^5\) 的陣列 \(a\),請將它排序好
先把大問題拆成小問題...
想排序 \([1,n]\),先排好 \([1,\frac{n}{2}],[\frac{n}{2}+1,n]\)
假設現在有兩個排序好的小陣列了
要把它們合併成一個排序好的大陣列
每次比較兩個小陣列最前面的數字
小的先拿
可以 \(O(n)\) 合併!
複雜度分析:
\(T(n)=2T(\frac{n}{2})+O(n)\)
\(T(n)=O(n\log n)\)
可以想像成每次把陣列切一半
共要切 \(\log\) 次
OJDL 7162
有個長度為 \(n\leq 10^5\) 的陣列 \(a(1\leq a_i\leq n)\)
問有多少區間 \([l,r](1\leq l\leq r\leq n)\)
滿足 \((\max(a_l,\dots,a_r))^2\geq \sum\limits_{i=l}^ra_i\)
對區間 \([1,n]\),把它分成左右各一半的小區間
答案有 \(3\) 種狀況:
\(l,r\) 都在左邊的區間
\(l,r\) 都在右邊的區間
\(l\) 在左,\(r\) 在右
前兩種可以遞迴處理
最後一種要想辦法計算跨越兩邊的答案
首先式子可以轉成
\((\max(a_l,\dots,a_r))^2 \geq \text{pre}_r-\text{pre}_{l-1}\)
先考慮最大值在左邊的情況
\(mx^2\geq \text{pre}_r-\text{pre}_{l-1}\)
移項變成
\(mx^2+\text{pre}_{l-1}\geq \text{pre}_r\)
對於一個 \(l\)
我們可以知道 \(mx^2+\text{pre}_{l-1}\) 是多少
\(\text{pre}_r\) 是遞增的
可以二分搜出 \(r\) 可以到哪裡
記得不要搜超過會讓最大值在右邊的 \(r\)
對於最大值在右邊的情況同理
\(T(n)=2T(\frac{n}{2})+O(n\log n)\)
\(T(n)=O(n\log^2(n))\)
一個問題能不能用分治做的關鍵是
它有沒有好的切法、有沒有一個好的合併方式
動態規劃
DP 在幹嘛
簡單來說就是設計狀態與狀態轉移
兩個東西看起來只有四個字
真正做起來卻可以非常難
把陣列取名成 dp
剛開始介紹 DP 一定會講到費氏數列
費氏數列:
最前面兩項都是 \(1\),
之後每項都是前面兩項總和的數列
設計狀態
\(dp_i\) 為數列第 \(i\) 項的值
注意 DP 時邊界的狀態要設好
\(dp_1=1,dp_2=1\)
狀態轉移
根據題目的不同
轉移方法也會有所差異
大致上來說會有三種
Bottom-Up (Pull)
計算這個狀態的答案時直接去找所有轉移來源
\(dp_i=dp_{i-1}+dp_{i-2}\)
缺點:有的時候只能從轉移來源推會轉移到誰
無法從會被轉移到的點找出誰轉移過來
Bottom-Up (Push)
我直接看我會轉移到誰
然後去更新它
\(dp_{i+1}\ +=\ dp_i\)
\(dp_{i+2}\ +=\ dp_i\)
Top-Down
寫一個遞迴函式,把已經算過的結果存下來
int dp[30];
int fib(int n) {
if (dp[n] != 0) return dp[n];
if (n <= 2) {
dp[n] = 1;
return dp[n];
}
dp[n] = fib(n - 1) + fib(n - 2);
return dp[n];
}
要怎麼練習 DP?
Atcoder DP contest 有很多 DP 經典題
CSES 的 DP section 的題目也很經典
接下來會從裡面的題目挑出幾個常見的基礎 DP
比 FFT 難的東西
有 \(N\leq 100\) 個物品,每個物品重 \(w_i\),價值 \(v_i\)
你有一個可以裝重量最多 \(W\leq 10^5\) 的背包
問在不超過重量限制下,裝的物品價值總和最高是多少
狀態設計
\(W\) 好像滿小的
那可以設 \(dp_{i,j}\) 代表
看完前 \(i\) 個物品以後,裝的重量不超過 \(j\) 的價值最大值
狀態轉移
遇到第 \(i\) 個物品,我可以選擇拿或不拿
\(dp_{i,j}=\max(dp_{i-1,j},dp_{i-1,j-w_i}+v_i)\)
時間複雜度 \(O(NW)\),空間複雜度 \(O(NW)\)
其實空間複雜度可以優化到 \(O(W)\)
我們每次都先做 \(dp_{i,j}=dp_{i-1,j}\)
其實就是把陣列複製一遍
現在不要複製,直接用同一個陣列
觀察到 \(dp_{i,j}=\max(dp_{i,j},dp_{i-1,j-w_i}+v_i)\)
如果 \(j\) 由大的掃到小的
那可以簡化成 \(dp_j=\max(dp_j,dp_{j-w_i}+v_i)\)
於是只要開一個一維陣列就好了!
以上的背包 DP 被稱為 0/1 背包問題
意思是可以取或不取這個物品
除了 0/1 背包以外
還有無限背包與多重背包
無限背包
一樣的問題,只是第 \(i\) 個物品從只能拿一個變成可以拿任意多個
狀態設計:與 0/1 背包一樣
狀態轉移:\(dp_{i,j}=\max(dp_{i-1,j},dp_{i,j-w_i}+v_i)\)
\(i,j\) 可以從 \(i,j-w_i\) 轉移過來
代表可以拿任意多個
一樣可以減少記憶體
\(dp_j=\max(dp_j,dp_{j-w_i}+v_i)\)
只是現在 \(j\) 要由小掃到大
最長共同子序列
給兩個長度 \(\leq 3000\) 的字串 \(s,t\)
請找一個長度最長的字串,且這個字串是 \(s,t\) 的子序列
子序列指的是原數列刪除若干個元素得到的東西
不一定要連續
設定狀態
\(dp_{i,j}\):\(s\) 看了前 \(i\) 個字元,\(t\) 看了前 \(j\) 個字元的最長共同子序列長度
邊界:\(dp_{i,0},dp_{0,j}\) 都設成 \(0\)
狀態轉移
\(dp_{i,j}\) 可以直接用 \(dp_{i-1,j}\) 或 \(dp_{i,j-1}\)
代表我直接不考慮 \(s\) 的第 \(i\) 個字元或 \(t\) 的第 \(j\) 個字元
如果 \(s_i=t_j\),那 \(dp_{i,j}\) 會是 \(dp_{i-1,j-1}+1\)
時間、空間複雜度 \(O(|s||t|)\)
可是題目要我們輸出一個答案耶
對於每個狀態,記錄它是從哪個狀態轉移過來的
最後從最佳解回溯
這題不需要另外開一個陣列記錄,因為只要 \(s_i=t_j\)
那我們就可以確定 \(i,j\) 是從 \(i-1,j-1\) 轉移過來的
否則直接走 \(dp_{i-1,j},dp_{i,j-1}\) 比較大的那一個
事實上如果只要求長度,那有空間 \(O(|t|)\) 的做法
最後會講到
其實要輸出答案也可以線性空間
有 \(N\leq 400\) 個史萊姆,每個史萊姆初始的大小為 \(a_1\sim a_N\)
合併兩隻相鄰、大小為 \(x,y\) 的史萊姆
需要花費 \(x+y\),新的史萊姆大小為 \(x+y\)
問把所有史萊姆合併成一隻的最小花費
設定狀態
\(dp_{l,r}\):把第 \(l\sim r\) 隻史萊姆合併的最小花費
答案為 \(dp_{1,N}\)
邊界:\(dp_{i,i}=0\)
狀態轉移
找一個 \(l\leq k<r\)
代表要把 \([l,k]\) 和 \([k+1,r]\) 兩隻史萊姆合併
\(dp_{l,r}=\min\limits_{l\leq k<r}(dp_{l,k}+dp_{k+1,r}+\sum\limits_{i=l}^r a_i)\)
\(\sum\limits_{i=l}^r a_i\) 可以用前綴和 \(O(1)\) 算
時間複雜度:
有 \(O(N^2)\) 個狀態,每個狀態要花 \(O(N)\) 轉移
共 \(O(N^3)\)
事實上,這題可以優化到 \(O(N^2)\) 甚至 \(O(N\log N)\)
遇到區間問題時可以想想看能不能區間 DP
從長度小狀態一直 DP 到長度大的
當 \(n\) 很小的時候
可以把位元當作狀態
有 \(n\leq 21\) 個男生和女生
\(a_{i,j}=1\) 代表第 \(i\) 個男生和第 \(j\) 個女生可以配對
總共要配 \(n\) 對,且一個人只能屬於一對
問方法數 \(\text{mod }10^9+7\)
設定狀態
\(dp_{\text{mask}}\) 代表前 \(\text{popcnt(mask)}\) 個男生
跟 \(\text{mask}\) 代表的女生配對的方法數
如果把配對到的女生寫成 \(1\),沒配對到的寫成 \(0\)
那可以寫成一個二進位的數字 \(010010\dots\)
我們稱這個二進位的數字為 \(\text{mask}\)
\(\text{mask}\) 的範圍會是 \(0\sim 2^n-1\)
邊界:\(dp_0=1\)
答案:\(dp_{2^n-1}\)
狀態轉移
\(\text{mask}\) 如果第 \(i\) 位的數字是 \(1\)
那可以令 \(\text{last}=\text{mask}\oplus 2^i\)
拿 \(dp_{\text{last}}\) 更新 \(dp_{\text{mask}}\)
時間複雜度:
有 \(O(2^n)\) 個狀態
每個要花 \(O(n)\) 轉移
共 \(O(n2^n)\)
練習題:TIOJ 2282
跟計算「數字」有關的都統稱為數位 DP
給一個很大的數字 \(K(K\leq 10^{10000})\) 跟 \(D\leq 100\)
問 \(1\sim K\) 裡的數字,有幾個數字的每一位總和是 \(D\) 的倍數
答案 \(\text{mod }10^9+7\)
注意到 \(K\) 很大,所以要拿字串存
此題的關鍵就是
是 \(D\) 的倍數 \(\Leftrightarrow\text{ mod }D=0\)
設定狀態
\(dp_{i,s,x}\)
\(i\):到 \(K\) 從左往右的第幾位
\(s\):\(0\) 或 \(1\),目前的前 \(i\) 位是否等於 \(K\) 的前 \(i\) 位
\(x\):目前的 \(i\) 個數字總和 \(\text{mod }D\) 是多少
邊界:\(dp_{0,1,0}=1\)
答案:\(dp_{|K|,0,0}+dp_{|K|,1,0}\)
狀態轉移
枚舉下一位是 \(0\sim 9\) 的哪一個
然後判一些 Case
因為會算到 \(0\),所以最後答案要減 \(1\)
複雜度:有 \(O(|K|D)\) 個狀態
每個狀態要花 \(B=10\) 轉移
共 \(O(|K|DB)\)(如果你不把 \(B\) 視為常數)
最長遞增子序列
給一個長度為 \(n\leq 2\times 10^5\) 的數列,輸出嚴格遞增子序列的最大長度
設定狀態
\(dp_i\):以 \(a_i\) 為結尾的最長長度
狀態轉移
\(dp_i=\max\limits_{1\leq j<i,a_j<a_i}(dp_j+1)\)
直接做要 \(O(n^2)\)
其中一個優化方法是離散化後開 BIT 取前綴 max
另外一個想法是維護一個陣列,第 \(i\) 個元素代表長度為 \(i\) 的遞增子序列最小的結尾數字
因為這個陣列是遞增的,所以每次加入 \(a_i\) 可以二分搜要更新到哪裡
最後的答案會是陣列的長度
複雜度 \(O(n\log n)\)
滾動 DP
滾動 DP 可以把 \(O(n^2)\) 的空間複雜度變成 \(O(n)\)
在求 LCS 的長度時,注意到 \(dp_{i,j}\) 只會從 \(dp_i,dp_{i-1}\) 轉移
之前的陣列都不會再被用到了
原本的 \(dp[i][j]\) 可以改成 \(dp[i \& 1][j]\)
\(dp[0]\) 代表偶數行的陣列,\(dp[1]\) 代表奇數行
#include <bits/stdc++.h>
using namespace std;
const int N = 3005;
int n, m;
int dp[N][N];
string s, t;
int main() {
ios::sync_with_stdio(false), cin.tie(0);
cin >> s >> t;
n = s.size(), s = " " + s;
m = t.size(), t = " " + t;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (s[i] == t[j]) dp[i & 1][j] = dp[!(i & 1)][j - 1] + 1;
else dp[i & 1][j] = max(dp[!(i & 1)][j], dp[i & 1][j - 1]);
}
}
cout << dp[n & 1][m] << '\n';
}
只有兩個長度為 \(n\) 的陣列
空間複雜度 \(O(n)\)!
結語:
DP 有非常多種,10 個題目有 11 種 DP 方法
要做的就是多練習題目,慢慢體會出要如何 DP
謝謝大家!