斜率优化DP学习笔记

一、简述斜率优化

初步了解

本文以luoguP3195 玩具装箱为例,我们很容易可以的出下面这个柿子:
\[ f_i = \min_{j = 1}^{i - 1} \{ f_j + (i - j - 1 + s_i - s_j - L) ^ 2 \} \]
\(b_i = s_i + i\)\(j\)\(f_i\) 的最优决策点,则有:
\[ f_i = f_j + (b_i - b_j - (L + 1)) ^ 2 \]
把只与 \(j\) 有关的放在左边:
\[ f_j + b_j + 2 b_j (L + 1) = 2 b_i b_j - b_i ^ 2 - (L + 1) ^ 2 + 2 b_i (L + 1) + f_i \]
\(b_j\) 看做横坐标,\(f_j + b_j + 2 b_j (L + 1)\) 看做纵坐标,这个式子就是一条经过点 \(\big( b_j, f_j + b_j + 2 b_j (L + 1) \big)\),斜率为 \(2 b_i\) 的直线,\(-b_i^2 - (L + 1) ^ 2 + 2 b_i (L + 1) + f_i\) 就看做截距。

斜率优化DP学习笔记_第1张图片

由于 \(b_i\) 是确定的,截距中除 \(f_i\) 以外的所有单项式都是常量,所以 \(j\) 是使该直线截距最小的点,可以看成一条斜率为 \(2 b_i\) 的直线从 \(-\infty\) 上移过程中碰到的第一个点。
可以维护一个下凸壳,使直线碰到的第一个点一定在凸壳上。

维护凸壳

斜率优化DP学习笔记_第2张图片

上图中,决策点的下凸壳为 \((B,C,E)\),但因为加入了点 \(G\),点 \(E\) 死了(大雾。
可以得到如下结论:
1、凸壳上边的斜率是从左到右单调递增的,否则会出现上图的情况。
2、设凸壳上的点从左至右依次记为 \(1\)~\(k\),显然有:在凸壳中加入一个横坐标大于当前所有点的点 \(t\),若 \(slope (k - 1, t) \leq slope (k - 1, k)\),则 \(k\) 不在凸壳上。
依照上述做法可以用单调栈来维护凸壳。

找决策点

对于斜率为 \(k\) 的直线,第一个碰到的点为凸壳上第一条斜率小于 \(k\) 的线段右端点,若没有则为最左边的点。
例题中,由于 \(2 b_i\) 的单调性,可以直接用单调队列把不符合上述条件的点扔掉。

一般情况

例题是斜率优化的入门题,当做横坐标和斜率的变量都有单调性,所以很好处理,在更加朴素的情况可以大致分为以下两种:
1、横坐标单调,斜率不单调:不用单调队列,在单调栈上二分即可,或者也可以打平衡树;
2、两者都不单调:我也没写过,平衡树维护应该问题不大。

一些例题

NOI2019d1t1 回家路线

\(Sol\)

\(f_i\) 表示乘坐第 \(i\) 班列车到达站点的最小代价。
暴力转移是显然的。
可以把 第\(i\) 班列车看作两个事件:
1、出发时刻:从决策点集中选点决策;
2、到达时刻:把第 \(i\) 个点加入所到达站点的决策点集。
\(2m\) 个时间按时间排序,就可以用单调队列维护。
\(p.s.\):由于时间不超过 \(1000\),直接暴力在凸包上选点可过 (考场亲测)
时间复杂度由于排序可以用 \(vector\)、链表等时间是线性的 \(O(m + T)\),其中 \(T\) 为最大时刻。

\(Source\)

#include 
#include 
#include 
#include 
int in() {
    int x = 0; char c = getchar(); bool f = 0;
    while (c < '0' || c > '9')
        f |= c == '-', c = getchar();
    while (c >= '0' && c <= '9')
        x = (x << 1) + (x << 3) + (c ^ 48), c = getchar();
    return f ? -x : x;
}
templateinline void chk_min(T &_, T __) { _ = _ < __ ? _ : __; }
templateinline void chk_max(T &_, T __) { _ = _ > __ ? _ : __; }

const int N = 1e5 + 5;

struct node {
    int x, t;
} a[N << 2];

std::vector q[N], b[2][1001];
int fro[N];

int n, m, A, B, C;
long long f[N << 1];

inline bool chk1(int j, int k, int M) {
    return f[j - m] - f[k - m] + A * (a[j].t * a[j].t - a[k].t * a[k].t) > M * (a[j].t - a[k].t);
}

inline bool chk2(int j, int k, int i) {
    return (f[j - m] - f[k - m] + A * (a[j].t * a[j].t - a[k].t * a[k].t)) * (a[k].t - a[i].t) >
           (f[k - m] - f[i - m] + A * (a[k].t * a[k].t - a[i].t * a[i].t)) * (a[j].t - a[k].t);
}

int main() {
    //freopen("in", "r", stdin);
    n = in(), m = in(), A = in(), B = in(), C = in();
    for (int i = 1, w, x, y, z; i <= m; ++i) {
        a[i] = (node){in(), in()};
        a[i + m] = (node){in(), in()};
        std::swap(a[i].t, a[i + m].x);
        b[0][a[i].t].push_back(i), b[1][a[i + m].t].push_back(i + m);
    }
    memset(f, -1, sizeof(f));
    for (int tim = 1, i; tim <= 1000; ++tim) {
        for (unsigned p = 0; p < b[0][tim].size(); ++p) {
            i = b[0][tim][p];
            if (a[i].x == 1)
                f[i] = A * a[i].t * a[i].t + B * a[i].t + C;
            while ((int)q[a[i].x].size() > fro[a[i].x] + 1) {
                int j = q[a[i].x][fro[a[i].x]], k = q[a[i].x][fro[a[i].x] + 1];
                if (a[i].t >= a[k].t && chk1(j, k, 2 * A * a[i].t + B))
                    ++fro[a[i].x];
                else
                    break;
            }
            if ((int)q[a[i].x].size() > fro[a[i].x]) {
                int t = q[a[i].x][fro[a[i].x]], tmp = a[i].t - a[t].t;
                if (!~f[i])
                    f[i] = f[t - m] + A * tmp * tmp + B * tmp + C;
                else
                    chk_min(f[i], f[t - m] + A * tmp * tmp + B * tmp + C);
            }

        }
        for (unsigned p = 0; p < b[1][tim].size(); ++p) {
            i = b[1][tim][p];
            if (~f[i - m]) {
                while ((int)q[a[i].x].size() > fro[a[i].x] + 1) {
                    int j = q[a[i].x][(int)q[a[i].x].size() - 2], k = q[a[i].x][(int)q[a[i].x].size() - 1];
                    if (chk2(j, k, i))
                        q[a[i].x].pop_back();
                    else
                        break;
                }
                q[a[i].x].push_back(i);
            }
        }
    }
    long long res = -1;
    for (int i = 1; i <= m; ++i)
        if (a[i + m].x == n && ~f[i])
            if (!~res)
                res = f[i] + a[i + m].t;
            else
                chk_min(res, f[i] + a[i + m].t);
    printf("%lld\n", res);
    return 0;
}

updating……

你可能感兴趣的:(斜率优化DP学习笔记)