• BZOJ 3156 防御准备


    Description

    Input

    第一行为一个整数N表示战线的总长度。

    第二行N个整数,第i个整数表示在位置i放置守卫塔的花费Ai。

    Output

    共一个整数,表示最小的战线花费值。

    Sample Input

    10
    2 3 1 5 4 5 6 3 1 2

    Sample Output

    18

    HINT

    1<=N<=10^6,1<=Ai<=10^9

      这道题显然是一道dp的题。一眼看去,显然可以令$f_i$表示第$i$个位置放防御塔的费用(先说明一下,为了方便计算,我把序列反向后从左到右计算),令$w_x=sum_{i=1}^{x}i$,那么显然有:$f_{i}=min{f_j+w_{i-j-1}}+a_{i}$。然而这是个$n^2$的dp方程,那么怎么做呢?翻了翻网上的题解,发现几乎都写了斜率优化。然而xzy神犇告诉我决策单调性可做。

      怎么做呢?不难发现对于位置$i$有两个决策$j,k(j<k)$满足$k$比$j$优,那么对于位置大于$i$的位置也满足这个。证明如下:

      若对于$j<k$且$f_j+w_{i-j-1}>f_k+w_{i-k-1}$

      因为$j<k$,所以对于$x>i$有$w_{x-j-1}-w_{i-j-1}>w_{x-k-1}-w_{i-k-1}$

      所以$f_j+w_{x-j-1}>f_k+w_{x-k-1}$

      于是我们就可以做了。我们维护一个单调队列,每次先把队首用不到的区间先弹掉,再用队首元素来更新当前答案,最后用这个解来更新后面的解。当我们发现 对于队尾区间的左端点当前解比队列中储存的解更优时,我们可以直接把这个区间给弹掉(想一想,为什么)。这样弹完以后,若队列中已经没有区间,我们就可以将当前解的区间直接加入队列;否则当前解最优的边界就在队尾的区间中,我们需要在这个区间内二分把这一个边界给找出来,然后更改这个区间的右边界并插入区间。

      这道题是我练习决策单调性优化dp的第一题,对于新手来说还是有一点难度的。代码如下:

     1 #include<iostream>
     2 #include<cstdio>
     3 #define File(s) freopen(s".in","r",stdin),freopen(s".out","w",stdout)
     4 #define maxn 1000010
     5 #define INF (1LL<<50)
     6 
     7 using namespace std;
     8 typedef long long llg;
     9 
    10 int n,a[maxn],l[maxn],r[maxn],x[maxn],lz,rz;
    11 llg f[maxn];
    12 
    13 int getint(){
    14     int w=0;bool q=0;
    15     char c=getchar();
    16     while((c>'9'||c<'0')&&c!='-') c=getchar();
    17     if(c=='-') q=1,c=getchar();
    18     while(c>='0'&&c<='9') w=w*10+c-'0',c=getchar();
    19     return q?-w:w;
    20 }
    21 
    22 inline llg sum(int j,int i){//用j决策来更新f[i] 
    23     return f[j]+((llg)(i-j)*(i-j-1)>>1);
    24 }
    25 
    26 int main(){
    27     File("a");
    28     n=getint();
    29     for(int i=n;i;i--) a[i]=getint(),f[i]=INF;
    30     f[1]=a[1]; f[n+1]=INF;
    31     l[rz]=2,r[rz]=n+1,x[rz++]=1;//第一个解需要先处理好
    32     for(int i=2;i<=n+1;i++){
    33         while(r[lz]<i && lz<rz) lz++;//更新当前解
    34         f[i]=sum(x[lz],i)+a[i];//从队尾弹掉没有当前解优的区间
    35         while(rz>lz && sum(i,l[rz-1])<=sum(x[rz-1],l[rz-1])) rz--;//修改并插入
    36         if(lz==rz) l[rz]=i+1,r[rz]=n+1,x[rz++]=i;//在队尾区间内二分
    37         else{
    38             int ll=l[rz-1],rr=r[rz-1]+1,mid,xx=x[rz-1];//修改并插入
    39             while(ll!=rr){
    40                 mid=ll+rr>>1;
    41                 if(sum(i,mid)<=sum(xx,mid)) rr=mid;
    42                 else ll=mid+1;
    43             }
    44             r[rz-1]=ll-1; l[rz]=ll,r[rz]=n+1,x[rz++]=i;//修改并插入
    45         }
    46     }
    47     printf("%lld",min(f[n],f[n+1]));
    48     return 0;
    49 }
    View Code

      UPD:斜率优化做法:

      其实这个式子可以推一推。由于$w_x=frac{x(x+1)}{2}$,所以有:$f_i=min{ f_j+frac{(i-j)(i-j-1)}{2} }+a_i$

      于是对于某一个$j$,有$2f_i-i^2+i-2a_i=2f_j+j^2+j-2ij$

      这就是个很显然的斜率式了。由于$i$、$j$单增,用一个单调队列维护下凸包即可。

      代码如下:

     1 #include<iostream>
     2 #include<cstdio>
     3 #include<cstring>
     4 #include<algorithm>
     5 #include<cmath>
     6 #define File(s) freopen(s".in","r",stdin),freopen(s".out","w",stdout)
     7 #define maxn 1000010
     8 
     9 using namespace std;
    10 typedef long long llg;
    11 
    12 struct data{
    13     llg x,y;
    14 }s[maxn];
    15 int n,a[maxn],d[maxn],l,r;
    16 llg f[maxn];
    17 
    18 int getint(){
    19     int w=0;bool q=0;
    20     char c=getchar();
    21     while((c>'9'||c<'0')&&c!='-') c=getchar();
    22     if(c=='-') c=getchar(),q=1;
    23     while(c>='0'&&c<='9') w=w*10+c-'0',c=getchar();
    24     return q?-w:w;
    25 }
    26 
    27 llg ji(int x){return (llg)x*(llg)x;}
    28 double xie(data x,data y){
    29     if(x.x==y.x) return 1e100;
    30     return (double)(y.y-x.y)/(double)(y.x-x.x);
    31 }
    32 
    33 int main(){
    34     File("a");
    35     n=getint();
    36     for(int i=n;i;i--) a[i]=getint();
    37     f[1]=a[1]; s[1].x=1; s[1].y=1+2*f[1]+1;
    38     l=r=1; d[1]=1;
    39     for(int i=2;i<=n+2;i++){
    40         while(l<r && xie(s[d[l]],s[d[l+1]])<=(i<<1)) l++;
    41         f[i]=f[d[l]]+a[i]+(ji(i-d[l])-(i-d[l]))/2;
    42         s[i].x=i; s[i].y=ji(i)+2*f[i]+i;
    43         while(l<r && xie(s[d[r-1]],s[d[r]])>=xie(s[d[r]],s[i])) r--;
    44         d[++r]=i;
    45     }
    46     printf("%lld",min(f[n],f[n+1]));
    47     return 0;
    48 }
    View Code
  • 相关阅读:
    2018年6月2号(线段树(2))
    [朋友(dalao)们的友链](¦3[▓▓]让我安详的躺一会儿...
    Hello,World
    算法笔记:数论基础
    [单源最短路]逃离僵尸岛
    算法笔记:最小生成树
    算法笔记:单调队列
    算法笔记:高斯消元
    [博客..配置?]博客园美化
    [大模拟]LuoGu P2033 Chessboard Dance
  • 原文地址:https://www.cnblogs.com/lcf-2000/p/5574163.html
Copyright © 2020-2023  润新知