進階DP,DP優化
Topics Covered:
- 單調隊列優化
- 斜率優化
- Slope Trick
- Permutation Trick*
- Aliens Trick
*Permutation Trick 是我自己命名的w
熱身一下吧
單調隊列優化
aka Sliding Window
當你要對每個東西找到前\(k\)個東西的最小值的時候...
用deque 維護
斜率優化
當你的DP式長這樣的時候...
\(dp[i] = max_{j < i}(m_j * x_i + k_j)\)
有沒有看起來很像一條直線?
\(dp[i] = max_{j < i}(m_j * x_i + k_j)\)
如果我們能保證 \(m_j, x_i\)遞增,那有什麼好的方法維護?
圖源:學長講義
"一路領先" 性質
實際上,我們需要支援兩種操作:
- 加入一條直線(或任何有優超性的函數)
- 詢問某個位置在所有函數下的最大值
優超性: 兩個函數\(f, g\)只在一個點相交
存在一個點\(p\),使得\(\)
每條線段都只有一個最好的區間
跟單調隊列類似的想法
把線段存進去!
-
新增一條線段時,看看他能不能把後面的線段「完全取代掉」
跟單調隊列類似的想法
把線段存進去!
-
看某個點的最佳線段時,從前面把已經不會更好的線段淘汰掉
直接來Commando 吧
給你一個正整數序列與一個開口向下的二次函數\(f\),你要把序列分成一些連續區間\(l_1-r_1, ..., l_k-r_k\),使得
最大
數學推一波
令 \(dp[i]\)為做完前\(i\)個的答案
原本的\(O(n^2)\)解: $$dp[i] = max_{j < i} (dp[j] + f(\sum_{k = j + 1}^{i} a[k]))$$
變成前綴和展開
變成前綴和展開
把左邊看成斜率為\(- 2 * a * p_j\)的直線,\(p_i\)做為詢問的\(x\)值,去找該\(x\)值最大\(y\)值的直線
題目保證\(p_j, p_i\)遞增,所以斜率跟\(x\)值都是遞增的
#include <iostream>
#include <algorithm>
#include <vector>
#include <deque>
#include <utility>
#define ll long long
#define pii pair<ll, ll>
#define ff first
#define ss second
using namespace std;
ll ax, bx, cx;
inline ll f(ll x) {
return ax * x * x + bx * x + cx;
}
inline ll val(pii a, ll x) {
return a.first * x + a.second;
}
inline bool comp(pii a, pii b, ll x) {
return val(a, x) <= val(b, x);
}
int main() {
int t;
cin >> t;
while (t--) {
ll n;
cin >>n;
cin >> ax >> bx >> cx;
ll arr[n], pref[n];
for (int i = 0;i < n;i++) {
cin >> arr[i];
pref[i] = arr[i] + (i ? pref[i - 1] : 0);
}
ll dp[n];
deque<pii > deq;
deq.push_back(make_pair(0, cx));
for (int i = 0;i < n;i++) {
while (deq.size() >= 2) {
pii cur = deq.front();
deq.pop_front();
pii rep = deq.front();
//cout << val(cur, pref[i]) << " " << val(rep, pref[i]) << endl;
if (!comp(cur, rep, pref[i])) {
deq.push_front(cur);
break;
}
}
/*
if (deq.size()) {
cout << deq.front().first << " " << deq.front().second <<endl;
}
*/
dp[i] = ax * pref[i] * pref[i] + bx * pref[i] + (deq.size() ? val(deq.front(), pref[i]) : 0);
//newline with -2a*pref[i], ax * pref[i] * pref[i] - b * pref[i] + pref[i] + dp[i])
pii add = make_pair(-2 * ax * pref[i], ax * pref[i] * pref[i] - bx * pref[i] + cx + dp[i]);
//cout << add.first << " " << add.ss << endl;
while (deq.size() >= 2) {
pii cur = deq.back();
deq.pop_back();
pii comp = deq.back();
if ((add.ss - comp.ss) * (comp.ff - cur.ff) <= (cur.ss - comp.ss) * (comp.ff - add.ff)) {
continue;
} else {
deq.push_back(cur);
break;
}
}
deq.push_back(add);
}
cout << dp[n - 1] << endl;
}
}
斜率不單調的話...
李超線段樹
對值域開一顆線段樹,每個節點內維護在該區間的中點數值最大的一條直線。
令這個位置為\(m[cur]\)
插入線段
藍色比較好,
紅色在左邊才有用
斜率: 紅 < 藍
紅色比較好,
藍色在右邊才有用
當前節點留下
比較好的線段,
比較差的下推!
比較兩線段在\(m[cur]\)的數值
單點查詢
從根節點開始,遇到線段就更新答案,
並往查詢位置的方向遞迴。
想法:
對於某節點的線段,他右邊的線段在\(m[cur]\)都會比較差,但在某個\(>m[cur]\)的地方會超過。
補充: 線段有轉移範圍?
只在轉移範圍的區間放線段,複雜度變\(O(\log^2 n)\)
或是你也可以寫
動態凸包
類題: 吐鈔機2
類題2:花枝遊戲
例題:CF 1603 D
例題: 2020 全國賽 pI - 黑白機
Slope Trick
看個例題: CF 713 C
給一個整數序列,每次操作可以選一個數字+1或-1,問最少需要幾次操作使序列嚴格遞增?
\(n \leq 3000, a_i \leq 10^9\)
\(O(n^2)\)? How about \(n \leq 10^5\)
糟糕作法
首先,讓\(a_i := a_i - i\),問題變成要多少次操作讓\(a\)變非嚴格遞增。
令\(f(i, j)\)代表讓前\(i\)項非嚴格遞增,最後一項\(\leq j\)的最小代價。那麼
\(f(i, j) = min(f(i-1, j) + |a_i - j|, f(i, j-1))\)
複雜度\(O(nC)\)欸,好爛
考慮滾動?
考慮以\(j\)為\(x\)軸,\(f(i, j)\)為縱軸的函數圖形。
從\(i-1\)變成\(i\)的時候,可以看成是把每個位置\(j\)加上\(|a_i - j|\),然後取前綴min!
ZCK: 你有聽過滾動DP嗎
用priority_queue 維護斜率改變的點!
假設最右邊斜率一定是零,則點\(x\)的斜率會是pq裡面比\(x\)大的數字個數
實作:
//default code
int a[maxn];
int main() {
io
int n;
cin >> n;
priority_queue<int, vector<int>, less<int> > pq;
ll ans = 0;
for (int i = 0;i < n;i++) cin >> a[i], a[i] -= i;
for (int i = 0;i < n;i++) {
pq.push(a[i]);
if (pq.top() > a[i]) {
pq.push(a[i]);
ans += pq.top() - a[i];
pq.pop();
}
//debug(pq.top());
}
cout << ans << endl;
}
其他: USACO Guide
Permutation Trick
酷酷題: JOIOC Skycraper
有\(n\)棟大樓,第\(i\)棟高度為\(a_i\)。給定\(L\),問有多少種方法將大樓排成一排,使得相鄰大樓的高度差總和\(\sum_{1 \leq i < n} |a_i - a_{i+1}| \leq L\)
\(n \leq 100, a_i, L \leq 1000\)
位元DP?
考慮每棟大樓對答案的貢獻
如果旁邊兩棟大樓都比他矮的話,貢獻\(2a_i\)
如果旁邊兩棟大樓都比他高的話,貢獻\(-2a_i\)
如果他在最左邊/右邊要特判
以此類推...
要怎麼枚舉排列?
由高到低放入!
先依\(a_i\)由大到小排序,假設算完前\(i-1\)項,新增\(a_i\)時旁邊的東西時需要知道哪些東西?
這裡之後會不會再放東西?
多一個維度
令\(dp[i][j][k][l]\)代表放入前\(i\)個東西,
下一項有\(j\)個中間的空格,\(k\)個兩邊的空格,
總代價為\(l\)的排列數。
轉移時,有放中間跟放旁邊兩種狀況:
放中間時,有少一個空格,沒改空格數,多一個空格三種...
放旁邊時以此類推...
最後答案是\(\sum_{l \leq L}dp[n][0][0][l]\)
這題的實作細節
實際上,因為我們\(l\)的狀態只能開到\(L \leq 1000\),所以必須讓\(dp\)過程中的總代價遞增。
因此在做\(dp[i]\)時,我們可以把每個空格的高度看成\(a_i\),這樣變成\(i+1\)時就能讓每個空格都多\(a_i - a_{i+1}\)的代價。
這顯然會遞增,我們就能在\(O(n^2L)\)做完這題。
Aliens優化
AKA 講師也不會的東西
UPD: 高三還是不太會
換個例題好了
我不想講AI-666了qwq
YTP 2021 程式挑戰賽 p6:
給一個整數陣列,選\(k\)個互不相鄰的數字使得總和最大。\(1 \leq k \leq n \leq 2 \times 10^5\)
簡單的二維DP: \(dp[i][j]\)代表前\(i\)個東西選\(j\)個的最大和。
觀察函數圖形
感覺起來拿越多東西,每個東西的作用會越來越少?
令\(f(k)\)為選\(k\)個東西的最大和,那麼
\(f(k+1) - f(k)\)遞減。
觀察函數圖形
先不考慮選\(k\)個的限制時,看一維的DP做法,可以得到選任意多個的最大值,也就是\(max(f(i))\)
假設現在選一個東西要多\(C\)的代價(可能是正或負),那麼一維DP仍然可做,得到的是\(max(f(i) + Ci)\)
並且因為差分單調(斜率遞減),我們可以二分搜\(C\)使得最大值發生在\(i=k\)
觀察函數圖形
Credit: Wiwiho
凸性的證明
可反悔Greedy!
\(k=1\)時,一定是選最大值。
假設後來不選他,一定要選到他旁邊兩個!
把這個「反悔」操作看成選一個東西。
2 4 3 5 -2
2 4 (3 + -2 - 5)
(2 + (-4) - 4)
顯然,每次選的最大值會非嚴格遞減,
也就符合了斜率單調。
OJDL: 疫苗分配
Credits/Other Resources
- ZCK DP 優化講義 (前面放的)
- Wiwiho 的簡報 (資讀社團內)
- USACO Guide
進階DP,DP優化
By justinlai2003
進階DP,DP優化
- 2,025