斜率优化之凸包优化与李超线段树
文章目錄
- 前言
- 凸包優化
- 第一步
- 第二步
- 最后一步
- 例一
- 轉移方程
- 凸包優化
- 代碼
- 例二
- 題目大意
- 轉移方程
- 凸包優化
- 代碼
- 李超線段樹
- 思想
- 插入
- 查詢
- 代碼
- 例三
- 代碼
- 例四
- 轉移方程
- 怎么做
- 代碼
前言
這種方法比傳統斜率優化更快,更準,更狠。
凸包優化
一切形如dp[i]=min?/max?{f1(j)?g1(i)+f2(j)}+g2(i)dp[i]=\min/\max\{f_1(j) \cdot g_1(i) + f_2(j)\} + g_2(i)dp[i]=min/max{f1?(j)?g1?(i)+f2?(j)}+g2?(i)的轉移方程,都可以凸包優化。
其中,fff為關于jjj的函數,ggg為關于iii的函數。
例如dp[i]=min?{?2hj?hi+hj2+dp[j]}+ai+hi2dp[i] = \min\{-2h_j \cdot h_i + {h_j}^2 + dp[j]\} + a_i + {h_i}^2dp[i]=min{?2hj??hi?+hj?2+dp[j]}+ai?+hi?2(這里面,f1(j)=?2hjf_1(j) = -2h_jf1?(j)=?2hj?,f2(j)=hj2+dp[j]f_2(j) = {h_j}^2 + dp[j]f2?(j)=hj?2+dp[j],g1(i)=hig_1(i) = h_ig1?(i)=hi?,g2(i)=ai+hi2g_2(i) = a_i + {h_i}^2g2?(i)=ai?+hi?2)
我們接下來口胡dp[i]=max?{f1(j)?g1(i)+f2(j)}+g2(i)dp[i]=\max\{f_1(j) \cdot g_1(i) + f_2(j)\} + g_2(i)dp[i]=max{f1?(j)?g1?(i)+f2?(j)}+g2?(i)的情況。
很簡單。
第一步
定義一個關于g1(i)g_1(i)g1?(i)和jjj的二元函數:Lj(g1(i))=f1(j)?g1(i)+f2(j)L_j\left(g_1(i)\right)=f_1(j) \cdot g_1(i) + f_2(j)Lj?(g1?(i))=f1?(j)?g1?(i)+f2?(j)為什么叫LjL_jLj?呢,因為這是一條直線,這條直線的斜率為f1(j)f_1(j)f1?(j),縱截距為f2(j)f_2(j)f2?(j)。
第二步
dp[i]=max?{Lj(g1(i))}+g2(i)dp[i]=\max\{L_j(g_1(i))\} + g_2(i)dp[i]=max{Lj?(g1?(i))}+g2?(i)也就是說,我們只需要找直線x=g1(i)x = g_1(i)x=g1?(i)與所有LjL_jLj?的交點中縱坐標最大的那個。
最后一步
用個李超線段樹即可。
但是,在大多數題你都會發現,f1f_1f1?和g1g_1g1?有單調性。
否則,用李超線段樹或CDQ或平衡樹什么的即可。
那么我接下來講f1f_1f1?單調減,g1g_1g1?單調增的情況吧。
再說一遍,很簡單。(你發現我們沒有進行任何計算)
現在要計算dp[i]dp[i]dp[i],則我們可以做到:此時已經按順序把所有Lj(1≤j<i)L_j(1\leq j <i)Lj?(1≤j<i)放進了一個雙端隊列QQQ,呈這個樣子(111指LQ[Head]L_{Q[Head]}LQ[Head]?,222指LQ[Head+1]L_{Q[Head + 1]}LQ[Head+1]?,以此類推):
加粗的地方是這個直線的“貢獻”,但有些直線沒有貢獻,例如下圖中的黑線:
基于歸納的思想,我們可以假設此時隊列中沒有這種線(?)(*)(?),然后在該次DP后維護這樣一個雙端隊列QQQ。
一個顯然的結論是:由于g1(i)g_1(i)g1?(i)單增,那么如果g1(i)g_1(i)g1?(i)到了這個地方,藍線就沒用了:
所以,不斷比較LQ[Head](g1(i))L_{Q[Head]}(g_1(i))LQ[Head]?(g1?(i))和LQ[Head+1](g1(i))L_{Q[Head + 1]}(g1(i))LQ[Head+1]?(g1(i)),來看LQ[Head]L_{Q[Head]}LQ[Head]?有沒有存在的必要,類似傳統斜率優化。
然后,考慮加入當前直線LiL_iLi?(下圖中的黑色),如果是這樣的,那么綠線就沒有用了(333指LQ[Tail]L_{Q[Tail]}LQ[Tail]?,222指LQ[Tail?1]L_{Q[Tail - 1]}LQ[Tail?1]?,以此類推):
這個問題的刻畫也很好想到:是比較 LiL_iLi?與LQ[Tail]L_{Q[Tail]}LQ[Tail]?的交點 和 LQ[Tail]L_{Q[Tail]}LQ[Tail]?與LQ[Tail?1]L_{Q[Tail - 1]}LQ[Tail?1]?的交點 的橫坐標。下圖中,若xA<xBx_A<x_BxA?<xB?,那LQ[Tail]L_{Q[Tail]}LQ[Tail]?就沒用了:
于是這樣就能做到(?)(*)(?)了,也是類似于傳統斜率優化。
說完了,看例題代碼有驚♂喜。
例一
Kalila and Dimna in the Logging Industry
轉移方程
不用看題,直接看轉移方程即可:dp[i]=min?1≤j<i{dp[j]+bj?ai}dp[i] = \min\limits_{1 \leq j < i}\{dp[j] + b_j \cdot a_i\}dp[i]=1≤j<imin?{dp[j]+bj??ai?}其中aia_iai?遞增,bib_ibi?遞減。
凸包優化
f1(j)=bjf_1(j)=b_jf1?(j)=bj?,g1(i)=aig_1(i)=a_ig1?(i)=ai?,f2(j)=dp[j]f_2(j)=dp[j]f2?(j)=dp[j],g2(i)=0g_2(i)=0g2?(i)=0,其中f1f_1f1?單減,g1g_1g1?單增,跟上面講的情況一模一樣。
代碼
#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; }例二
Hit the Coconuts
題目大意
你想打開zzz個椰子吃,你的沙比隊友給你準備了nnn個椰子,每個椰子的堅硬♂程度不同,第iii個椰子的堅硬♂程度是aia_iai?,表示它要被敲aia_iai?下才能被打開(不一定要連續敲)。 你不知道椰子的順序。 請問至少要敲多少下才能打開最少zzz個椰子。
有必要看一下樣例:
第一個:抓一個直接敲55下,不管怎么樣都能敲開;
第二個:抓一個,先敲40下,如果沒開,就拿另一個敲40下,至少能得到1個椰子。
轉移方程
我都沒看出來是個DP。
先排個序,然后先考慮怎么敲開一個椰子:
記陰影矩形的面積為SiS_iSi?,如果我們想撬開1個椰子,那敲min?{Si}\min\{S_i\}min{Si?}下就行了,因為對于任意一種ai×(n?i+1)a_i\times(n-i+1)ai?×(n?i+1)下的方案,必定能敲出一個椰子:先隨便找個椰子敲aia_iai?下,如果沒打開,就換一個沒敲過的再敲,重復此操作,臉再黑也就是把陰影部分倒著敲完,那也能把第iii個敲開。
接下來考慮,如果我們想敲開兩個椰子,答案是min?i<j{Si∪Sj}\min\limits_{i<j}\{S_i\cup S_j\}i<jmin?{Si?∪Sj?}。
考慮你是一個黑人的情況:先敲了SiS_iSi?下才敲開一個椰子,那你的椰子變成了這樣:
然后,你肯定知道哪些是敲過的,你就在敲過的那些里面敲SjS_jSj?下,就又打開了一個椰子。
于是問題轉變為在矩形里面找面積最小的,含zzz級的階梯的階梯形(我是倒著來的):dp[i][j]=min?k>j{dp[i?1][k]+aj?(k?j)}dp[i][j] = \min\limits_{k>j}\{dp[i - 1][k] + a_j\cdot(k-j)\}dp[i][j]=k>jmin?{dp[i?1][k]+aj??(k?j)}
凸包優化
dp[i][j]=min?k>j{dp[i?1][k]+k?aj}?aj?jdp[i][j] = \min\limits_{k>j}\{dp[i - 1][k] + k\cdot a_j\}-a_j\cdot jdp[i][j]=k>jmin?{dp[i?1][k]+k?aj?}?aj??jf1(k)=kf_1(k)=kf1?(k)=k,f2(k)=dp[i?1][k]f_2(k)=dp[i - 1][k]f2?(k)=dp[i?1][k],g1(j)=ajg_1(j) = a_jg1?(j)=aj?,g2(j)=aj?jg_2(j) = a_j\cdot jg2?(j)=aj??j,注意iii跟凸包優化無關,是j,kj,kj,k參與凸包優化。
由于我倒著來的,所以f1f_1f1?單減,g1g_1g1?單減,然后就簡單了。
代碼
#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; }李超線段樹
如果f1f_1f1?和g1g_1g1?沒有單調性,我們就不能用雙端隊列維護了。
李超線段樹的作用很簡單:維護一些一次函數(直線 / 線段),支持插入和查詢,查詢時可以找到當前橫坐標下最大 / 最小的函數值。
完美解決幾乎所有凸包優化。
代碼只有40行。
思想
它每個區間記錄的是該區間中點處的最大函數值對應的函數MaxiMax_iMaxi?。
插入
插入直線curcurcur的過程如下:
- curcurcur在這個區間上完全覆蓋了MaxiMax_iMaxi?:將MaxiMax_iMaxi?變成curcurcur,返回(沒有懶標記,不用再改兒子,看查詢的過程就知道了);
- 如果該區間中點處Maxi(mid)<cur(mid)Max_i(mid)<cur(mid)Maxi?(mid)<cur(mid),則交換MaxiMax_iMaxi?和curcurcur,保證MaxiMax_iMaxi?的意義正確;
- 現在的curcurcur會對交點所在子樹產生貢獻(下圖中,右子樹的橙色段需要修改),因此遞歸下去:
查詢
比較簡單,遞歸得到下層的答案,跟自己這層比(因此不用插入和查詢都可以不用懶標記)即可。
代碼
見例題,有驚♂喜。
例三
[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否覆蓋了Lowreturn 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; }例四
Jump mission
轉移方程
dp[i]=min?j<i且pj<pi{dp[j]+(hi?hj)2}+aidp[i]=\min\limits_{_{j<i\text{且}p_j<p_i}}\{dp[j]+(h_i-h_j)^2\}+a_idp[i]=j<i且pj?<pi??min?{dp[j]+(hi??hj?)2}+ai?其中ppp不單調,hhh不單調,aaa不單調。
怎么做
看到這個題,什么都不單調,還尼瑪有轉移限制???
不可做,溜了。
正解:樹狀數組套李超樹維護凸包。
樹狀數組中,每個結點是一個李超樹,維護對應區間的凸包。查詢的時候,從pip_ipi?用lowbit減到000,根據樹狀數組的性質,訪問到的恰好就是dp[i]dp[i]dp[i]的所有轉移直線,統計最大的函數值即可。(其實樹狀數組很大的一個用處就是處理偏序問題,一定程度上可以替代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; }總結
以上是生活随笔為你收集整理的斜率优化之凸包优化与李超线段树的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 斜率优化 笔记
- 下一篇: html页面漏斗图,echarts 漏斗