zoukankan      html  css  js  c++  java
  • Slope Trick:解决一类凸代价函数DP优化

    【前言】

    在补Codeforce的DP时遇到一个比较新颖的题,然后在知乎上刚好 hycc 桑也写了这道题的相关题解,这里是作为学习并引用博客的部分内容

    这道题追根溯源发现2016年这个算法已经在APIO2016烟花表演与Codeforces 713C引入,自那之后似乎便销声匿迹了。相关题型数量也较少,因而在这里结合前辈们的工作做一些总结。---by hycc

    问题引入:Codeforces 713C

    题目链接:Here

    题意:

    • 给定 (n) 个正整数 (a_i) ,每次操作可以选择任意一个数将其 (+1)(-1) ,问至少需要多少次操作可以使得 (n) 个数保持严格单增

    • 数据范围:(1le nle 3000,1le a_ile 10^9)

    对我来说这道题其实和曾经写过的 POJ-3666:求不升的DP是一样的

    这个题是求升序的DP,那么有什么变化呢

    不升的条件是:(a_i -a_j ge 0)

    升序的条件是:(a_i -a_j ge i - j) 对任意 (i,j) 均满足

    有没有理解到什么?移项有:(a_i - i ge a_j - j)

    所以将 (a)​ 数字变形一下就和POJ3666就是一个题!

    【AC Code】

    const int N = 3100;
    int n, m;
    ll f[N][N], a[N], b[N];
    int main() {
        cin.tie(nullptr)->sync_with_stdio(false);
        cin >> n;
        for (int i = 1; i <= n; ++i) {
            cin >> a[i];
            a[i] = a[i] - i;
            b[i] = a[i];
        }
        sort(b + 1, b + 1 + n);
        m = 1;
        for (int i = 2; i <= n; ++i) if (b[i] != b[i - 1]) b[++m] = b[i];
        memset(f, 0, sizeof(f));
        for (int i = 1; i <= n; ++i) {
            ll Min = LLONG_MAX;
            for (int j = 1; j <= m; j++) {
                Min = min(Min, f[i - 1][j]);
                f[i][j] = abs(b[j] - a[i]) + Min;
            }
        }
        ll ans = LLONG_MAX;
        for (int i = 1; i <= m; ++i) ans = min(ans, f[n][i]);
        cout << ans << "
    ";
    }
    

    当然上面说的思路并不是本篇博客实际想表达,以下才是正文

    对于朴素的 (mathcal{O}(n^2) DP)​ :

    一个显然的性质:如果不是“严格单增”而是“严格非降”,那么最终形成的严格非降序列,其中每个元素一定属于 ({a_i})

    将元素离散化后可以设计 (f_{i,j}) 表示到第 (i) 个数取 (j) 的最少操作数

    那么有转移 (f_{i,j} = minlimits_{kle j}f_{i-1,k} + | a_i - j|)​ ,记录 (f_{i-1,*})​ 的前缀 (min)​ 即可做到 (mathcal{O}(n^2))

    至于如何做到“严格非降”,(a_{i-1} < a_i,a_{i -1} le a_i - i,a_{i-1}-(i-1)le a_i - i)

    于是令 (a_i = a_i - i) 即可。

    赛后的评论区中出现了一种 (mathcal{O}(Nlog N)) 的做法,也就是 Slope Trick算法的第一次现身(?)


    Slope Trick:解决一类凸代价函数的DP优化问题

    当序列DP的转移代价函数为

    连续
    分段线性函数
    凸函数

    时,可以通过记录分段函数的最右一段 (f_r(x)) 以及其分段点 (L)​ 实现快速维护代价的效果。

    如:(f(x)=left{egin{array}{rr} -x-3 & (x leq-1) \ x & (-1<x leq 1) \ 2 x-1 & (x>1) end{array} ight.)

    可以仅记录 (f_r(x) = 2x - 3) 与分段点 (L_f = {-1,-1,1}) 来实现对该分段函数的存储。

    注意:要求相邻分段点之间函数的斜率差为 (1) ,也就是说相邻两段之间斜率差 (ge 1) 的话,这个分段点要在序列里出现多次。

    优秀的性质:

    (F(x),G(x)) 均为满足上述条件的分段线性函数,那么 (H(x) =F(x)+G(x)) 同样为满足条件的分段线性函数,且 (H_r(x) = F_r(x) + G_r(x),L_H = L_F igcup L_G)

    该性质使得我们可以很方便得运用数据结构维护 (L)​ 序列。

    回顾:Codeforces 713C

    转移方程为 (f_{i,j} = minlimits_{kle j}f_{i-1,k} + |a_i - j|)​​

    (F_{i}(x)=f_{i, x}, G_{i}(x)=minlimits _{k leq x} f_{i-1, k}=min limits_{k leq x} F_{i-1}(k))

    那么有 (F_i(x) = G_i(x) + |x -a_i|) ,其中 (F_i,G_i) 均为分段线性函数。

    (G_i) 求的是 (F_{i-1}) 的关于函数值的前缀最小值,由于 (F_{i-1}) 是一个凸函数,因而其最小值应该在斜率 (=0) 处取得,其后部分可以舍去。

    而每次由 (G_i(x)) 加上 (|x-a_i|) ,等价于在 (L) 中添加两个分段点 ({a_i,a_j})

    因而 (G_i) 各段的函数斜率形如 ({...,-3,-2,-1,0}) ,加上 $|x-a_i| $后斜率变为 ({...,-3,-2,-1,0,1}) ,因而需要删除末尾的分段点。

    具体实现中:使用大根堆维护分段点单调有序,每次加入两个 (a_i) ,再弹出堆顶元素。

    总复杂度 :(mathcal{O}(n log n))


    [QAQ ]


    Codeforces 1534G

    题意:

    一个无限大的二维平面上存在 (n)​ 个点 ((x_i,y_i))​ 均需要被访问一次,从 ((0,0)) 出发,每次可以向右或向上移动一个单位。

    可以在任意位置 ((X,Y)) 访问 ((x_i,y_i)) 并付出 (max{|X-x_i|,|Y-y_i|}) 的代价(访问后依然留在 ((X,Y)) )。同一位置可以访问多个点。

    问:至少需要花费多少代价才能使得所有点均被访问?

    数据范围: (1le nle 800000,0le x_i,y_ile 10^9)

    借用Hycc桑的配图

    结合上图可以看出,对于点 ((X,Y)) ,一定会选择路径与直线 (x+y=X+Y)(红线)的交点 ((x,y)) 处作为访问的发起点(在这条线上 (|X-x| = |Y-y|) )。

    考虑到这条红线是倾斜的,因而将坐标系顺时针翻转 (45^°)​,即 ((x+y,x-y)) 代替 ((x,y))

    此时,每次移动变为 ((x+1,y-1))​ 或 ((x+1,y+1))

    把所有点按新的 (x) 坐标排序,即可转为序列上的问题。

    设值域为 (M) ,则很容易写出 (mathcal{O}(nM)) 的转移方程:

    (f_{i,Y}) 表示从左到右考虑到横坐标为 (x_i) 的所有点,当前路径到了 ((x_i,Y)) 的最小代价,

    那么有

    $f_{i,Y}=minlimits_{Y-left|x_{i}-x_{i-1} ight|leq kleq Y+left|x_{i}-x_{i-1} ight|}f_{i-1, k}+sumlimits_{(x, y), x=x_{i}}|Y-y| $​​

    同样,设 (F_{i}(x)=f_{i, x}, G_{i}(x)=sumlimits_{x-left|x_{i}-x_{i-1} ight| leq k leq x+left|x_{i}-x_{i-1} ight|} f_{i-1, k})

    那么 (F_{i}(x)=G_{i}(x)+sum_{left(x^{prime}, y^{prime} ight), x^{prime}=x_{i}}left|x-y^{prime} ight|)


    主要问题在于 (G_i(x)) 的维护,是取一个区间范围 ([L,R]) 内的最小值。

    若斜率为 (0) 的两端点在 ([L,R]) 内,那么直接取最小值即可。

    若斜率为 (0) 的两端点在 (L) 左侧,需要取 (L) 处的值作为最小值。

    若斜率为 (0) 的两端点在 (R)​ 右侧,需要取 (R) 处的值作为最小值。

    因而,需要维护斜率为 (0) 的折线的两侧分割点 ((a,b)) ,同时还需要支持从斜率为 (0) 处向两侧访问,因而使用小根堆与大根堆分别维护 (b) 右侧以及 (a) 左侧的点。

    每次添加新的分割点时,根据新分割点与 (a,b) 的大小关系决定插入小根堆or大根堆,同时调整 (a,b) ,每次调整复杂度是 (mathcal{O}(1)) 的(从小根堆中取出塞入大根堆或反之)

    【AC Code】借用 jiangly的代码

    #include <bits/stdc++.h>
    using i64 = long long;
    int main() {
        std::ios::sync_with_stdio(false);
        std::cin.tie(nullptr);
        int n;
        std::cin >> n;
        std::vector<std::pair<int, int>> a;
        for (int i = 0; i < n; i++) {
            int x, y;
            std::cin >> x >> y;
            a.emplace_back(x + y, x);
        }
        std::priority_queue<i64> hl;
        std::priority_queue<i64, std::vector<i64>, std::greater<>> hr;
        for (int i = 0; i < n + 5; i++) {
            hl.push(0);
            hr.push(0);
        }
        i64 tag = 0, mn = 0;
        int last = 0;
        std::sort(a.begin(), a.end());
        for (auto [s, x] : a) {
            int d = s - last;
            last = s;
            tag += d;
            if (x <= hl.top()) {
                mn += hl.top() - x;
                hl.push(x);
                hl.push(x);
                hr.push(hl.top() - tag);
                hl.pop();
            } else if (x >= hr.top() + tag) {
                mn += x - (hr.top() + tag);
                hr.push(x - tag);
                hr.push(x - tag);
                hl.push(hr.top() + tag);
                hr.pop();
            } else {
                hl.push(x);
                hr.push(x - tag);
            }
        }
        std::cout << mn << "
    ";
        return 0;
    }
    

    The desire of his soul is the prophecy of his fate
    你灵魂的欲望,是你命运的先知。

  • 相关阅读:
    51nod 1463 找朋友 (扫描线+线段树)
    51nod 1295 XOR key (可持久化Trie树)
    51nod 1494 选举拉票 (线段树+扫描线)
    51Nod 1199 Money out of Thin Air (树链剖分+线段树)
    51Nod 1287 加农炮 (线段树)
    51Nod 1175 区间中第K大的数 (可持久化线段树+离散)
    Codeforces Round #426 (Div. 1) B The Bakery (线段树+dp)
    前端基础了解
    git 教程
    HIVE 默认分隔符 以及linux系统中特殊字符的输入和查看方式
  • 原文地址:https://www.cnblogs.com/RioTian/p/15181170.html
Copyright © 2011-2022 走看看