進階DP,DP優化
Topics Covered:
- 單調隊列優化
- 斜率優化
- Slope Trick
- Permutation Trick*
- Aliens Trick
*Permutation Trick 是我自己命名的w
熱身一下吧
單調隊列優化
aka Sliding Window
當你要對每個東西找到前k個東西的最小值的時候...

用deque 維護
斜率優化
當你的DP式長這樣的時候...
dp[i]=maxj<i(mj∗xi+kj)
有沒有看起來很像一條直線?
dp[i]=maxj<i(mj∗xi+kj)
如果我們能保證 mj,xi遞增,那有什麼好的方法維護?

圖源:學長講義
"一路領先" 性質
實際上,我們需要支援兩種操作:
- 加入一條直線(或任何有優超性的函數)
- 詢問某個位置在所有函數下的最大值
優超性: 兩個函數f,g只在一個點相交
存在一個點p,使得
每條線段都只有一個最好的區間
跟單調隊列類似的想法
把線段存進去!
-
新增一條線段時,看看他能不能把後面的線段「完全取代掉」

跟單調隊列類似的想法
把線段存進去!
-
看某個點的最佳線段時,從前面把已經不會更好的線段淘汰掉

直接來Commando 吧
給你一個正整數序列與一個開口向下的二次函數f,你要把序列分成一些連續區間l1−r1,...,lk−rk,使得
最大
數學推一波
令 dp[i]為做完前i個的答案
原本的O(n2)解: dp[i]=maxj<i(dp[j]+f(k=j+1∑ia[k]))
變成前綴和展開
變成前綴和展開
把左邊看成斜率為−2∗a∗pj的直線,pi做為詢問的x值,去找該x值最大y值的直線
題目保證pj,pi遞增,所以斜率跟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(log2n)
或是你也可以寫
動態凸包
類題: 吐鈔機2
類題2:花枝遊戲
例題:CF 1603 D
例題: 2020 全國賽 pI - 黑白機
Slope Trick
看個例題: CF 713 C
給一個整數序列,每次操作可以選一個數字+1或-1,問最少需要幾次操作使序列嚴格遞增?
n≤3000,ai≤109
O(n2)? How about n≤105
糟糕作法
首先,讓ai:=ai−i,問題變成要多少次操作讓a變非嚴格遞增。
令f(i,j)代表讓前i項非嚴格遞增,最後一項≤j的最小代價。那麼
f(i,j)=min(f(i−1,j)+∣ai−j∣,f(i,j−1))
複雜度O(nC)欸,好爛
考慮滾動?
考慮以j為x軸,f(i,j)為縱軸的函數圖形。
從i−1變成i的時候,可以看成是把每個位置j加上∣ai−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棟高度為ai。給定L,問有多少種方法將大樓排成一排,使得相鄰大樓的高度差總和∑1≤i<n∣ai−ai+1∣≤L
n≤100,ai,L≤1000
位元DP?
考慮每棟大樓對答案的貢獻
如果旁邊兩棟大樓都比他矮的話,貢獻2ai
如果旁邊兩棟大樓都比他高的話,貢獻−2ai
如果他在最左邊/右邊要特判
以此類推...
要怎麼枚舉排列?
由高到低放入!
先依ai由大到小排序,假設算完前i−1項,新增ai時旁邊的東西時需要知道哪些東西?
這裡之後會不會再放東西?
多一個維度
令dp[i][j][k][l]代表放入前i個東西,
下一項有j個中間的空格,k個兩邊的空格,
總代價為l的排列數。
轉移時,有放中間跟放旁邊兩種狀況:
放中間時,有少一個空格,沒改空格數,多一個空格三種...
放旁邊時以此類推...
最後答案是∑l≤Ldp[n][0][0][l]
這題的實作細節
實際上,因為我們l的狀態只能開到L≤1000,所以必須讓dp過程中的總代價遞增。
因此在做dp[i]時,我們可以把每個空格的高度看成ai,這樣變成i+1時就能讓每個空格都多ai−ai+1的代價。
這顯然會遞增,我們就能在O(n2L)做完這題。
Aliens優化
AKA 講師也不會的東西
UPD: 高三還是不太會
換個例題好了
我不想講AI-666了qwq
YTP 2021 程式挑戰賽 p6:
給一個整數陣列,選k個互不相鄰的數字使得總和最大。1≤k≤n≤2×105
簡單的二維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,105