前言
這種方法比傳統斜率優化更快,更準,更狠。
凸包優化
一切形如的轉移方程,都可以凸包優化。
其中,爲關於的函數,爲關於的函數。
例如(這裏面,,,,)
我們接下來口胡的情況。
很簡單。
第一步
定義一個關於和的二元函數:爲什麼叫呢,因爲這是一條直線,這條直線的斜率爲,縱截距爲。
第二步
也就是說,我們只需要找直線與所有的交點中縱座標最大的那個。
最後一步
用個李超線段樹即可。
但是,在大多數題你都會發現,和有單調性。
否則,用李超線段樹或CDQ或平衡樹什麼的即可。
那麼我接下來講單調減,單調增的情況吧。
再說一遍,很簡單。(你發現我們沒有進行任何計算)
現在要計算,則我們可以做到:此時已經按順序把所有放進了一個雙端隊列,呈這個樣子(指,指,以此類推):
加粗的地方是這個直線的“貢獻”,但有些直線沒有貢獻,例如下圖中的黑線:
基於歸納的思想,我們可以假設此時隊列中沒有這種線,然後在該次DP後維護這樣一個雙端隊列。
一個顯然的結論是:由於單增,那麼如果到了這個地方,藍線就沒用了:
所以,不斷比較和,來看有沒有存在的必要,類似傳統斜率優化。
然後,考慮加入當前直線(下圖中的黑色),如果是這樣的,那麼綠線就沒有用了(指,指,以此類推):
這個問題的刻畫也很好想到:是比較 與的交點 和 與的交點 的橫座標。下圖中,若,那就沒用了:
於是這樣就能做到了,也是類似於傳統斜率優化。
說完了,看例題代碼有驚♂喜。
例一
Kalila and Dimna in the Logging Industry
轉移方程
不用看題,直接看轉移方程即可:其中遞增,遞減。
凸包優化
,,,,其中單減,單增,跟上面講的情況一模一樣。
代碼
#include <algorithm>
#include <cstdio>
#include <cstring>
typedef long long LL;
const int MAXN = 100000;
const LL INF = 1ll << 60;
int N;
LL A[MAXN + 5], B[MAXN + 5];
LL Dp[MAXN + 5];
struct Line {
LL k, b;
Line() { }
Line(LL _k, LL _b) { k = _k, b = _b; }
LL Calc(int x) { return k * x + b; } // 算函數值
double Ints(Line other) { // 求兩直線交點的橫座標
return (double)(other.b - b) / (k - other.k);
}
}Q[MAXN + 5];
int Head, Tail;
int main() {
scanf("%d", &N);
for (int i = 1; i <= N; i++)
scanf("%lld", A + i);
for (int i = 1; i <= N; i++)
scanf("%lld", B + i);
Q[Head = Tail = 1] = Line(B[1], 0); // 邊界注意一下即可
for (int i = 2; i <= N; i++) {
int x = A[i];
while (Tail - Head + 1 >= 2 && Q[Head].Calc(x) >= Q[Head + 1].Calc(x))
Head++;
Dp[i] = Q[Head].Calc(x); // 找到x=A[i]處的最低點
Line cur(B[i], Dp[i]);
while (Tail - Head + 1 >= 2 && Q[Tail].Ints(cur) <= Q[Tail].Ints(Q[Tail - 1]))
Tail--;
Q[++Tail] = cur; // 加入Li
}
printf("%lld\n", Dp[N]);
return 0;
}
例二
題目大意
你想打開個椰子喫,你的沙比隊友給你準備了個椰子,每個椰子的堅硬♂程度不同,第個椰子的堅硬♂程度是,表示它要被敲下才能被打開(不一定要連續敲)。 你不知道椰子的順序。 請問至少要敲多少下才能打開最少個椰子。
有必要看一下樣例:
Input
2
2 1
50 55
2 1
40 100
Output
55
80
第一個:抓一個直接敲55下,不管怎麼樣都能敲開;
第二個:抓一個,先敲40下,如果沒開,就拿另一個敲40下,至少能得到1個椰子。
轉移方程
我都沒看出來是個DP。
先排個序,然後先考慮怎麼敲開一個椰子:
記陰影矩形的面積爲,如果我們想撬開1個椰子,那敲下就行了,因爲對於任意一種下的方案,必定能敲出一個椰子:先隨便找個椰子敲下,如果沒打開,就換一個沒敲過的再敲,重複此操作,臉再黑也就是把陰影部分倒着敲完,那也能把第個敲開。
接下來考慮,如果我們想敲開兩個椰子,答案是。
考慮你是一個黑人的情況:先敲了下才敲開一個椰子,那你的椰子變成了這樣:
然後,你肯定知道哪些是敲過的,你就在敲過的那些裏面敲下,就又打開了一個椰子。
於是問題轉變爲在矩形裏面找面積最小的,含級的階梯的階梯形(我是倒着來的):
凸包優化
,,,,注意跟凸包優化無關,是參與凸包優化。
由於我倒着來的,所以單減,單減,然後就簡單了。
代碼
#include <algorithm>
#include <cstdio>
#include <cstring>
typedef long long LL;
const int MAXN = 1000;
int N, Z; LL H[MAXN + 5];
LL Dp[MAXN + 5][MAXN + 5];
struct Line {
LL k, b;
Line() { }
Line(LL x, LL y) { k = x, b = y; }
LL Calc(int x) {
return k * x + b;
}
double Ints(Line other) {
return (double)(b - other.b) / (other.k - k);
}
}Q[MAXN + 5];
int Head, Tail;
/*
1
3 2
1 8 10
*/
int main() {
int T; scanf("%d", &T);
while (T--) {
scanf("%d%d", &N, &Z);
for (int i = 1; i <= N; i++)
scanf("%lld", &H[i]);
std::sort(H + 1, H + 1 + N);
for (int i = 1; i <= N; i++)
Dp[1][i] = (N - i + 1) * H[i];
for (int i = 2; i <= Z; i++) {
Q[Head = Tail = 1] = Line(N - i + 2, Dp[i - 1][N - i + 2]);
for (int j = N - i + 1; j >= 1; j--) { // 注意邊界
int x = H[j];
while (Tail - Head + 1 >= 2 && Q[Tail].Calc(x) >= Q[Tail - 1].Calc(x))
Tail--;
Dp[i][j] = Q[Tail].Calc(x) - H[j] * j;
Line cur(j, Dp[i - 1][j]); // 當前層是加上一層的直線 通過轉移方程就能看出來
while (Tail - Head + 1 >= 2 && Q[Tail].Ints(cur) <= Q[Tail].Ints(Q[Tail - 1]))
Tail--;
Q[++Tail] = cur;
}
}
LL Ans = 1ll << 60;
for (int i = 1; i <= N - Z + 1; i++)
Ans = std::min(Ans, Dp[Z][i]);
printf("%lld\n", Ans);
}
return 0;
}
李超線段樹
如果和沒有單調性,我們就不能用雙端隊列維護了。
李超線段樹的作用很簡單:維護一些一次函數(直線 / 線段),支持插入和查詢,查詢時可以找到當前橫座標下最大 / 最小的函數值。
完美解決幾乎所有凸包優化。
代碼只有40行。
思想
它每個區間記錄的是該區間中點處的最大函數值對應的函數。
插入
插入直線的過程如下:
- 在這個區間上完全覆蓋了:將變成,返回(沒有懶標記,不用再改兒子,看查詢的過程就知道了);
- 如果該區間中點處,則交換和,保證的意義正確;
- 現在的會對交點所在子樹產生貢獻(下圖中,右子樹的橙色段需要修改),因此遞歸下去:
查詢
比較簡單,遞歸得到下層的答案,跟自己這層比(因此不用插入和查詢都可以不用懶標記)即可。
代碼
見例題,有驚♂喜。
例三
[JSOI2008]Blue Mary開公司
這是一道版題。
代碼
#include <algorithm>
#include <cstdio>
#include <cstring>
const int MAXT = 100000;
const int MAXX = 50000;
const double INF = 1e9;
struct LiChao_Tree {
#define lch (i << 1)
#define rch (i << 1 | 1)
struct Line {
double k, b;
inline double Calc(int x) {
return k * x + b;
}
}Max[MAXT + 5];
inline bool Cover(Line Low, Line High, int x) { // 判斷x處Hight否覆蓋了Low
return Low.Calc(x - 1) <= High.Calc(x - 1);
}
void Insert(int i, int l, int r, Line cur) {
if (Cover(Max[i], cur, l) && Cover(Max[i], cur, r)) {
Max[i] = cur;
return;
}
if (l == r)
return;
int mid = (l + r) >> 1;
if (Cover(Max[i], cur, mid))
std::swap(Max[i], cur);
if (Cover(Max[i], cur, l))
Insert(lch, l, mid, cur);
if (Cover(Max[i], cur, r))
Insert(rch, mid + 1, r, cur);
}
double Query(int i, int l, int r, int x) {
double tmp = -INF;
int mid = (l + r) >> 1;
if (x < mid)
tmp = Query(lch, l, mid, x);
if (x > mid)
tmp = Query(rch, mid + 1, r, x);
return std::max(tmp, Max[i].Calc(x - 1));
}
}Tree;
int main() {
int T, X; scanf("%d", &T);
while (T--) {
char opt[20];
scanf("%s", opt);
if (opt[0] == 'P') {
LiChao_Tree::Line tmp;
scanf("%lf%lf", &tmp.b, &tmp.k);
Tree.Insert(1, 1, MAXX, tmp);
}
else {
scanf("%d", &X);
printf("%d\n", int(Tree.Query(1, 1, MAXX, X) / 100));
}
}
return 0;
}
例四
轉移方程
其中不單調,不單調,不單調。
怎麼做
看到這個題,什麼都不單調,還尼瑪有轉移限制???
不可做,溜了。
正解:樹狀數組套李超樹維護凸包。
樹狀數組中,每個結點是一個李超樹,維護對應區間的凸包。查詢的時候,從用lowbit
減到,根據樹狀數組的性質,訪問到的恰好就是的所有轉移直線,統計最大的函數值即可。(其實樹狀數組很大的一個用處就是處理偏序問題,一定程度上可以替代CDQ分治)
代碼
#include <algorithm>
#include <cstdio>
#include <cstring>
typedef long long LL;
const int MAXN = 300000;
const int MAXL = 600000;
const LL INF = 1ll << 60;
struct Line {
LL k, b;
Line() { k = 0, b = INF; }
Line(LL _k, LL _b) { k = _k, b = _b; }
LL Calc(int x) { return k * x + b; }
double Ints(Line other) {
return (double)(other.b - b) / (k - other.k);
}
};
struct LiChao_Tree {
#define lch (Child[i][0])
#define rch (Child[i][1])
Line Min[MAXN * 20 + 5];
int NodeCnt;
int Child[MAXN * 20 + 5][2];
inline bool Cover(Line Low, Line High, int x) {
return Low.Calc(x) <= High.Calc(x);
}
void Insert(int &i, int l, int r, Line cur) {
if (!i)
i = ++NodeCnt;
if (Cover(cur, Min[i], l) && Cover(cur, Min[i], r)) {
Min[i] = cur;
return;
}
if (l == r)
return;
int mid = (l + r) >> 1;
if (Cover(cur, Min[i], mid))
std::swap(Min[i], cur);
if (Cover(cur, Min[i], l))
Insert(lch, l, mid, cur);
if (Cover(cur, Min[i], r))
Insert(rch, mid + 1, r, cur);
}
LL Query(int i, int l, int r, int x) {
LL tmp = INF;
int mid = (l + r) >> 1;
if (x < mid)
tmp = Query(lch, l, mid, x);
if (x > mid)
tmp = Query(rch, mid + 1, r, x);
return std::min(tmp, Min[i].Calc(x));
}
#undef lch
#undef rch
}Tree;
struct BIT {
#define lowbit(x) ((x) & (-(x)))
int Root[MAXN + 5];
void Update(int p, Line l) {
for (int i = p; i <= MAXN; i += lowbit(i))
Tree.Insert(Root[i], 1, MAXL, l);
}
LL GetMin(int p, int x) {
LL ret = INF;
for (int i = p; i > 0 ; i -= lowbit(i))
ret = std::min(ret, Tree.Query(Root[i], 1, MAXL, x));
return ret;
}
#undef lowbit
}CHT;
int N, P[MAXN + 5];
LL A[MAXN + 5], H[MAXN + 5];
LL Dp[MAXN + 5];
int main() {
scanf("%d", &N);
for (int i = 1; i <= N; i++)
scanf("%d", &P[i]);
for (int i = 1; i <= N; i++)
scanf("%lld", &A[i]);
for (int i = 1; i <= N; i++)
scanf("%lld", &H[i]);
CHT.Update(P[1], Line(-2 * H[1], A[1] + H[1] * H[1]));
for (int i = 2; i <= N; i++) {
Dp[i] = CHT.GetMin(P[i], H[i]) + A[i] + H[i] * H[i];
CHT.Update(P[i], Line(-2 * H[i], Dp[i] + H[i] * H[i]));
}
printf("%lld", Dp[N]);
return 0;
}