• 学习笔记·斜率优化 [HNOI2008]玩具装箱


    (qwq)今天(rqy)给窝萌这些蒟蒻讲了斜率优化……大概是他掉打窝萌掉打累了吧顺便偷了(rqy)讲课用的图

    (Step 1) 一点小转化

    事实上斜率优化是专门用来处理这样一类(dp)式子的 $$dp_i = A_i + max limits _{j = 1}^{i -1}(B_j - C_j imes base_i)$$窝萌尝试把上式中的(B_j)(C_j)(base_i)等价成(x_j)(y_j)(k_i),并把它们丢到一个平面上,然后它萌就会变成一堆点((x_j,y_j)),画一条过他们的直线,类似于$$y - y_j = k_i(x - x_j)$$变换一下$$y = k_ix + (y_j + x_j)$$窝萌会发现,现在窝萌所求的不过是一条截距最大的直线而已。那么其实就是相当于求一个给定(k)意义下最靠上的点。

    (qwq)那么窝萌不妨先减弱问题的不可做性,使其单调——令(x)单调增、(k_i)单调减。

    那当窝萌在朴素(DP)遍历所有的(k_i)时,所得到的直线的轨迹应该是这样的:

    (上图是个(GIF)……不动的话就拖出来看吧)轨迹正好是一个凸壳,并且你会发现它的轨迹正好是再绕着每个斜率下最优的点旋转。

    (rqy)对此是这么解释的:

    可以发现好多点是没有机会作为最优的点的。

    形象⼀点的说,如果⼀个点的左边和右边某两个点在它上⽅“搭起”了⼀条线段,那么它就永远不会被选到。

    而因为我们保证了(x)(k)的单调性,所以就我们可以比较方便的考虑“如何选取当前最优点”这个问题。我们考虑遇到一个新的点,是否把他加入最优解集合里面,其实质就是维护一个上凸壳。那么已知两个点(A)(B),现在遇到了新加入的点(C),此时有两种情况:

    1、(BC)的斜率大于(AB)的斜率

    这时我们需要抛弃(B),直接连接(AC)

    2、(BC)的斜率小于(AB)的斜率

    通过这种方式我们就可以完成对整个凸壳的维护。而在判定时也很简单,用叉积来判断即可,即有(A)(B)(C)三个点分别是((x_A,y_A),(x_B,y_B),(x_C,y_C))满足(x_A leq x_B leq x_C),那么如果$$(x_C-x_A)(y_B - y_A) - (x_B - x_A)(y_C- y_A) < 0$$则证明是第一种情况,否则是第二种。

    (emmmm)直接求斜率当然也是可以的吧,不过会不会很慢很麻烦啊(qwq)

    (Step 2) 代码实现以及拓展

    (emmm)我们考虑用一个逻辑上单调的队列来实现去掉不优的状态 +加入新的状态。回归上题,我们所求的是(max),所以我们需要去掉那些斜率小的状态;同时需要加入斜率大的状态。由于整个过程牵扯到前后删点,所以用双端队列来维护。本蒟蒻的(伪)代码如下:

    int queue[MAXN] ;
    int head = 0, tail = 0 ;
    for(i = 1; i <= N; i ++){
    	while (1){
    		A = queue[head], B = queue[head + 1] ;
    		if (y[A] - k[i] * x[A] < y[B] - k[i] * x[B]) head ++ ;
    		else break ;
    	}
    	dp[i] = base[i] + y[A] - k[i] * x[A] ;
    	Maybe y[i] needs calcing ?
    	Maybe x[i] also needs calcint ?
    	So, Calc_it() ;
    	while (tail - head >= 2){
    		A = queue[tail - 1], B = queue[tail] ;
    		if ((y[i] - y[A]) * (x[B] - x[A])
    		  - (x[i] - x[A]) * (y[B] - y[A]) > 0) 
    		  	tail-- ;
    		else break ;
    	}
    	queue[++ tail] = i ;
    }
    

    对了……我在啃这个地方时出了个(bug)……那是因为我一直以为(y)(x)从本质上来讲有(n^2)个……(OTZ)

    那么对于拓展而言,我们以上做的一切都是基于“(x)(k)单调”这一前提的,那么还会有以下两种情况:

    1、(x)不单调

    那么实质上就是我们需要动态插入删除、从内部删除,那么就需要用平衡树来维护一个凸包了 。

    2、(k)不单调

    那么实质上就是我们原来比较方便的第一个(while)——从左往右直接删是不行的了,因为现在不优不代表之后不优,所以此时我们需要的就是三分一个位置而不是从前向后扫、

    哦,还有,对于(dp)式子而言,如果它长这个样子:$$dp_i = A_i + min limits _{j = 1}^{i -1}(B_j - C_j imes base_i)$$那么其实我们维护的就是一个下凸壳,所以此时只需要把所有的大于号改成小于号即可。

    嗝……那么下面就来看题吧


    #(mathcal{color{red}{Description}})

    (Link)

    给定一个序列(C)和一个常数(P) 。你要将序列划分成若干段;每一段如果从(L)(R)、整段的和为(S) ,则会产生((R-L+S-P)^2)的代价,求最小代价。

    #(mathcal{color{red}{Solution}})

    对于这个题而言,我们考虑先把这个恶心的式子化简一下:$$(R-L+S-P)^2 = (R -L + sum limits_{i=L}^{R}{C_i} +P)^2 = (sum limits_{i=L}^{R}{C_i + 1} - (P + 1)) ^ 2$$然后我们发现这玩意儿只要我们一开始把输入的所有(C_i)(P++)就好了……但我并没有这么做(qwq)……因为这是(rqy)的做法(hhhhh)

    回归刚才的,我们观察原式其实可以得出不同的式子,比如我们把((sum limits _{i=L}^{R}{C_i + 1} - (P + 1))^2)写成 ((S - L) ^ 2),那么就是$$(sum_R - sum{L-1} + (R - L - 1)-P-1)^2$$而对于比较朴素的(dp)方程来讲,他大概长这样$$dp_i = min(dp_j + (sum_R - sum_{L} + (R - L)-P-1)^2)$$窝萌不妨记一个(A_i = sum_i + i,B_i = sum_i+i+P+1),那么原来的方程就可以写作$$dp_i = min(dp_j + (A_i-B_j)^2)$$稍微展开一下就是$$dp_i = dp_j+A_i^2 + B_j^2 - 2A_iB_j$$然后你就会发现跟斜率优化的板子们惊人的相似……其中(A_i) 只跟(i) 有关,看作模型中的(A_i) (常项),(dp_j+B_j^2) 看作模型中的(B_j)(B_j)看作模型中的(C_j)(2A_i) 看作模型中的(base_i),即斜率(k),跑一个斜率优化(DP)即可。

    #include <cstdio>
    #include <iostream>
    #define MAXN 200010
    
    using namespace std ; int T1, T2 ;
    int N, L, head = 1, tail = 1, que[MAXN], i ;
    double S[MAXN], A[MAXN], B[MAXN], dp[MAXN] ;
    
    inline double qr(){
        int k = 0 ; char c = getchar() ;
        while (c < '0' || c > '9') c = getchar() ;
        while (c <= '9' && c >= '0') k = (k << 1) + (k << 3) + c - 48, c = getchar() ;
        return (double)k ;
    }
    inline double get_x(int now){return B[now] ;}
    inline double get_y(int now){return dp[now] + B[now] * B[now] ;}
    inline double get_K (double a, double b){
        return ( get_y(a) - get_y(b) )/ ( get_x(a) - get_x(b) ) ;
    }
    int main(){
        cin >> N >> L ;
        for (i = 1; i <= N; i ++) S[i] = qr() + S[i - 1] ; 
        for (i = 0; i <= N; i ++) A[i] = S[i] + i, B[i] = A[i] + L + 1 ;
        for (i = 1; i <= N; i ++){
            while (head < tail && get_K(que[head], que[head + 1]) < 2 * A[i]) ++ head  ; 
            dp[i] = dp[que[head]] + (A[i] - B[que[head]]) * (A[i] - B[que[head]]) ;
            while (head < tail && get_K(que[tail - 1], i) < get_K(que[tail], que[tail - 1])) -- tail  ;
            que[++ tail] = i ;
        }
        cout << ((long long)dp[N]);
    }
    ```
  • 相关阅读:
    JavaScript Patterns 5.7 Object Constants
    JavaScript Patterns 5.6 Static Members
    JavaScript Patterns 5.5 Sandbox Pattern
    JavaScript Patterns 5.4 Module Pattern
    JavaScript Patterns 5.3 Private Properties and Methods
    JavaScript Patterns 5.2 Declaring Dependencies
    JavaScript Patterns 5.1 Namespace Pattern
    JavaScript Patterns 4.10 Curry
    【Android】如何快速构建Android Demo
    【Android】如何实现ButterKnife
  • 原文地址:https://www.cnblogs.com/pks-t/p/9418134.html
Copyright © 2020-2023  润新知