斜率优化之凸包优化与李超线段树

馋奶兔 提交于 2019-12-28 11:05:12

前言

这种方法比传统斜率优化更快,更准,更狠。

凸包优化

一切形如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;
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!