斜率優化之凸包優化與李超線段樹

前言

這種方法比傳統斜率優化更快,更準,更狠。

凸包優化

一切形如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)的轉移方程,都可以凸包優化。
其中,ff爲關於jj的函數,gg爲關於ii的函數。
例如dp[i]=min{2hjhi+hj2+dp[j]}+ai+hi2dp[i] = \min\{-2h_j \cdot h_i + {h_j}^2 + dp[j]\} + a_i + {h_i}^2(這裏面,f1(j)=2hjf_1(j) = -2h_jf2(j)=hj2+dp[j]f_2(j) = {h_j}^2 + dp[j]g1(i)=hig_1(i) = h_ig2(i)=ai+hi2g_2(i) = a_i + {h_i}^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)的情況。

很簡單。

第一步

定義一個關於g1(i)g_1(i)jj的二元函數: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)爲什麼叫LjL_j呢,因爲這是一條直線,這條直線的斜率爲f1(j)f_1(j),縱截距爲f2(j)f_2(j)

第二步

dp[i]=max{Lj(g1(i))}+g2(i)dp[i]=\max\{L_j(g_1(i))\} + g_2(i)也就是說,我們只需要找直線x=g1(i)x = g_1(i)與所有LjL_j的交點中縱座標最大的那個。

最後一步

用個李超線段樹即可。
但是,在大多數題你都會發現,f1f_1g1g_1有單調性。
否則,用李超線段樹或CDQ或平衡樹什麼的即可。

那麼我接下來講f1f_1單調減,g1g_1單調增的情況吧。
再說一遍,很簡單。(你發現我們沒有進行任何計算)
真的


現在要計算dp[i]dp[i],則我們可以做到:此時已經按順序把所有Lj(1j<i)L_j(1\leq j <i)放進了一個雙端隊列QQ,呈這個樣子(11LQ[Head]L_{Q[Head]}22LQ[Head+1]L_{Q[Head + 1]},以此類推):
這個樣子
加粗的地方是這個直線的“貢獻”,但有些直線沒有貢獻,例如下圖中的黑線:
黑線
基於歸納的思想,我們可以假設此時隊列中沒有這種線()(*),然後在該次DP後維護這樣一個雙端隊列QQ

一個顯然的結論是:由於g1(i)g_1(i)單增,那麼如果g1(i)g_1(i)到了這個地方,藍線就沒用了:
這個地方
所以,不斷比較LQ[Head](g1(i))L_{Q[Head]}(g_1(i))LQ[Head+1](g1(i))L_{Q[Head + 1]}(g1(i)),來看LQ[Head]L_{Q[Head]}有沒有存在的必要,類似傳統斜率優化。

然後,考慮加入當前直線LiL_i(下圖中的黑色),如果是這樣的,那麼綠線就沒有用了(33LQ[Tail]L_{Q[Tail]}22LQ[Tail1]L_{Q[Tail - 1]},以此類推):
這樣的
這個問題的刻畫也很好想到:是比較 LiL_iLQ[Tail]L_{Q[Tail]}的交點LQ[Tail]L_{Q[Tail]}LQ[Tail1]L_{Q[Tail - 1]}的交點橫座標。下圖中,若xA<xBx_A<x_B,那LQ[Tail]L_{Q[Tail]}就沒用了:
下圖
於是這樣就能做到()(*)了,也是類似於傳統斜率優化。


說完了,看例題代碼有驚♂喜。
強

例一

Kalila and Dimna in the Logging Industry

轉移方程

不用看題,直接看轉移方程即可:dp[i]=min1j<i{dp[j]+bjai}dp[i] = \min\limits_{1 \leq j < i}\{dp[j] + b_j \cdot a_i\}其中aia_i遞增,bib_i遞減。

凸包優化

f1(j)=bjf_1(j)=b_jg1(i)=aig_1(i)=a_if2(j)=dp[j]f_2(j)=dp[j]g2(i)=0g_2(i)=0,其中f1f_1單減,g1g_1單增,跟上面講的情況一模一樣。

代碼

#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

題目大意

你想打開zz個椰子喫,你的沙比隊友給你準備了nn個椰子,每個椰子的堅硬♂程度不同,第ii個椰子的堅硬♂程度是aia_i,表示它要被敲aia_i下才能被打開(不一定要連續敲)。 你不知道椰子的順序。 請問至少要敲多少下才能打開最少zz個椰子。
有必要看一下樣例:

Input
2
2 1
50 55 
2 1
40 100
Output
55
80

第一個:抓一個直接敲55下,不管怎麼樣都能敲開;
第二個:抓一個,先敲40下,如果沒開,就拿另一個敲40下,至少能得到1個椰子。

轉移方程

我太菜了
我都沒看出來是個DP。


先排個序,然後先考慮怎麼敲開一個椰子:
這樣考慮
記陰影矩形的面積爲SiS_i,如果我們想撬開1個椰子,那敲min{Si}\min\{S_i\}下就行了,因爲對於任意一種ai×(ni+1)a_i\times(n-i+1)下的方案,必定能敲出一個椰子:先隨便找個椰子敲aia_i下,如果沒打開,就換一個沒敲過的再敲,重複此操作,臉再黑也就是把陰影部分倒着敲完,那也能把第ii個敲開。

接下來考慮,如果我們想敲開兩個椰子,答案是mini<j{SiSj}\min\limits_{i<j}\{S_i\cup S_j\}
敲兩個🥥
考慮你是一個黑人的情況:先敲了SiS_i下才敲開一個椰子,那你的椰子變成了這樣:
這樣
然後,你肯定知道哪些是敲過的,你就在敲過的那些裏面敲SjS_j下,就又打開了一個椰子。


於是問題轉變爲在矩形裏面找面積最小的,含zz級的階梯的階梯形(我是倒着來的):dp[i][j]=mink>j{dp[i1][k]+aj(kj)}dp[i][j] = \min\limits_{k>j}\{dp[i - 1][k] + a_j\cdot(k-j)\}

凸包優化

dp[i][j]=mink>j{dp[i1][k]+kaj}ajjdp[i][j] = \min\limits_{k>j}\{dp[i - 1][k] + k\cdot a_j\}-a_j\cdot jf1(k)=kf_1(k)=kf2(k)=dp[i1][k]f_2(k)=dp[i - 1][k]g1(j)=ajg_1(j) = a_jg2(j)=ajjg_2(j) = a_j\cdot j,注意ii跟凸包優化無關,是j,kj,k參與凸包優化。
由於我倒着來的,所以f1f_1單減,g1g_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;
}

李超線段樹

如果f1f_1g1g_1沒有單調性,我們就不能用雙端隊列維護了。
李超線段樹的作用很簡單:維護一些一次函數(直線 / 線段),支持插入和查詢,查詢時可以找到當前橫座標下最大 / 最小的函數值
完美解決幾乎所有凸包優化。

代碼只有40行。
豁害

思想

它每個區間記錄的是該區間中點處的最大函數值對應的函數MaxiMax_i

插入

插入直線curcur的過程如下:

  • curcur在這個區間上完全覆蓋了MaxiMax_i:將MaxiMax_i變成curcur,返回(沒有懶標記,不用再改兒子,看查詢的過程就知道了);覆蓋
  • 如果該區間中點處Maxi(mid)<cur(mid)Max_i(mid)<cur(mid),則交換MaxiMax_icurcur,保證MaxiMax_i的意義正確;交換cur和Maxi
  • 現在的curcur會對交點所在子樹產生貢獻(下圖中,右子樹的橙色段需要修改),因此遞歸下去:橙色段需要修改

查詢

比較簡單,遞歸得到下層的答案,跟自己這層比(因此不用插入和查詢都可以不用懶標記)即可。

代碼

見例題,有驚♂喜。

例三

[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;
}

例四

Jump mission

轉移方程

dp[i]=minj<ipj<pi{dp[j]+(hihj)2}+aidp[i]=\min\limits_{_{j<i\text{且}p_j<p_i}}\{dp[j]+(h_i-h_j)^2\}+a_i其中pp不單調,hh不單調,aa不單調。

怎麼做

看到這個題,什麼都不單調,還尼瑪有轉移限制???
???
不可做,溜了。


正解:樹狀數組套李超樹維護凸包

樹狀數組中,每個結點是一個李超樹,維護對應區間的凸包。查詢的時候,從pip_ilowbit減到00,根據樹狀數組的性質,訪問到的恰好就是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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章