斜率DP十连测


最近学校里很多题目都没时间做,顺便来写一下博客
斜率DP十连:

A[征途]

注意到,原题的式子,等价于 m a i 2 ( a i ) 2 后面部分是常数
那么我们就可以写出dp方程 f [ i , j ] = m i n { f [ i 1 , k ] + ( s j s k ) 2 } , k < j
对每个i单独做斜率dp,那么式子改写为 f i = m i n { 2 s j s i + f j + s j 2 + s i 2 }
斜率为 s i ,维护下凸包即可

#include<stdio.h>
#include<string.h>
#include<algorithm>
#define db double
#define LL long long 
using namespace std;
int n,m,q[3010],T; LL f[2][3010],s[3010];
inline LL y(int j){
    return f[T^1][j]+s[j]*s[j];
}
inline db slp(int j,int k){
    return (y(j)-y(k))/(db)(2.*(s[j]-s[k])); 
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;++i) scanf("%lld",s+i),s[i]+=s[i-1];
    for(int i=1;i<=m;++i){
        int l=0,r=0; T=i&1;
        for(int j=1;j<=n;++j){
            while(l<r && slp(q[l],q[l+1])<s[j]) ++l; int k=q[l];
            f[T][j]=y(k)+s[j]*s[j]-2*s[j]*s[k]; 
            while(l<r && slp(q[r-1],q[r])>slp(q[r],j)) --r;
            q[++r]=j;
        }
    }
    printf("%lld\n",m*f[m&1][n]-s[n]*s[n]);
}




B[货币兑换]

是一个神仙题也
首先注意到一个贪心策略,每天要么全部买入,全部卖出或者什么也不做
好的,注意到这一点之后就可以考虑怎么dp了,设 f i 表示到了第i天最多有多少钱
x i 表示到了第i天最多有多少a股, y i 表示同时最多有多少b股
我们有三种转移
1.今天什么也不做 f i = f i 1
2.今天卖掉从某一天买来的股票 f i = m a x { a i x j + b i y j } , j < i
3.买入股票 y i = f i a i r a t e i + b i , x i = y i r a t e i
重点关注第二条式子,我们将其改写:

y j = a i b i x j + f i b i

这里b是常数可以不管,只需要维护上凸包,让后在凸包上二分斜率 a i b i 即可
但是发现 a i b i 不是单调的,我们需要选择以下一项:
1.splay维护凸包
2.cdq分治
前者明显比后者直观易懂,所以我们用splay维护凸包,过程和用队列差不多,只是如果一个点被加入到了凸包内部需要直接删除,而加入的有用的点,直接找它左边和右边第一个满足上凸的点,并将中间的都删掉即可

#include<math.h>
#include<stdio.h>
#include<string.h>
#include<algorithm>
#define N 100010
#define db double
#define eps 1e-9
#define inf 1e9
#define son(x) (s[f[x]][1]==x)
using namespace std;
int n,s[N][2],v[N],f[N],rt;
db l[N],r[N],a[N],b[N],X[N],Y[N],g[N],rat[N];
inline db slp(int j,int k){
    return fabs(X[j]-X[k])<eps?-inf:(Y[j]-Y[k])/(double)(X[j]-X[k]);
}
inline void rot(int x){
    int y=f[x],d=son(x),g=f[y];
    s[y][d]=s[x][!d]; f[s[y][d]]=y;
    if(g) s[g][son(y)]=x; f[x]=g;
    s[x][!d]=y; f[y]=x;
}
inline void splay(int x,int r=0){
    for(int p;(p=f[x])!=r;rot(x))
        if(f[p]!=r && son(x)==son(p)) rot(p);
    if(!r) rt=x;
}
inline void insert(int k){
    if(!rt){ rt=k; return; }
    for(int x=rt;;){
        int d=X[x]<X[k];
        if(!s[x][d]){ s[x][d]=k; f[k]=x; splay(k); return; }
        else x=s[x][d];
    }
}
inline int gPre(int k){
    int r=0;
    for(int x=s[k][0];x;){
        if(slp(x,k)<=l[x]+eps) r=x,x=s[x][1];
        else x=s[x][0];
    }
    return r;
}
inline int gSuc(int k){
    int l=0;
    for(int x=s[k][1];x;){
        if(slp(x,k)+eps>=r[x]) l=x,x=s[x][0];
        else x=s[x][1];
    }
    return l;
}
inline void ps(int k){
    int p;
    if(s[k][0]){
        p=gPre(k);
        splay(p,k);
        f[s[p][1]]=0;
        s[p][1]=0;
        l[k]=r[p]=slp(k,p);
    } else l[k]=inf;
    if(s[k][1]){
        p=gSuc(k);
        splay(p,k);
        f[s[p][0]]=0;
        s[p][0]=0;
        r[k]=l[p]=slp(k,p);
    } else r[k]=-inf;
}
inline void maintain(int k){
    splay(k); ps(k); int p;
    if(l[k]<=r[k]+eps){
        if(!s[k][0] || !s[k][1]){
            rt=s[k][0]+s[k][1];
            f[rt]=0; ps(rt);
        } else {
            p=gSuc(k);
            splay(p,k);
            rt=s[k][0];
            f[rt]=0;
            s[rt][1]=p;
            f[p]=rt;
            ps(p); ps(rt);
        }
    }
}
inline void out(int x){
    if(!x) return;
    out(s[x][0]);
    printf("%d ",x);
    out(s[x][1]);
}
inline int find(db k){
    if(!rt) return 0;
    int x=rt;
    for(;;){
        if(r[x]>k+eps) x=s[x][1]; else
        if(l[x]+eps<k) x=s[x][0];
        else return x;
    }
}
int main(){
    scanf("%d%lf",&n,g);
    for(int i=1;i<=n;++i) scanf("%lf%lf%lf",a+i,b+i,rat+i);
    for(int i=1;i<=n;++i){
        int j=find(-a[i]/b[i]);
        g[i]=max(g[i-1],a[i]*X[j]+b[i]*Y[j]);
    //  printf("%d\n",j);
        Y[i]=g[i]/(a[i]*rat[i]+b[i]);
        X[i]=Y[i]*rat[i]; insert(i); maintain(i);// out(rt); puts("");
    }
    for(int i=n;i<=n;++i) printf("%.3lf\n",g[i]);
}



C[柠檬]

不得不说这题如果想到了关键一步就基本秒掉了,然而我并没有想到
首先考虑一下怎么转移式子
f i 表示前i个贝壳能转化为多少柠檬,枚举j,发现各种无法转移
因为我们不可能去枚举 s 0 那怎么办呢
注意到决策最优性,如果对于某一段,我们选取大小为 s 0 的贝壳去做转化,那么这一段的开头和结尾必然也是 s 0 否则我们将其分成独立的一段,一定更优
这下马上可以转移了,写成方程就是

f i = m a x { f j 1 + v i ( s i s j + 1 ) 2 } , v i = v j

这里 v i 贝壳i的大小, s i 表示的是i是第几个大小为 v i 的贝壳
那么对于每个v[i]我们维护一个单独的上凸壳,每次询问在上面二分即可,这里用vector节省空间

#include<vector>
#include<stdio.h>
#include<string.h>
#include<algorithm>
#define LL long long 
#define db double
using namespace std;
vector<int> q[10010];
int n,c[10010],v[100010]; LL f[100010],s[100010];
inline LL y(int j){
    return f[j-1]+v[j]*s[j]*s[j]-2*s[j]*v[j];
}
inline db slp(int j,int k){
    return (y(j)-y(k))/(2.*v[j]*(s[j]-s[k]));
}
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;++i){
        scanf("%d",v+i);
        s[i]=s[c[v[i]]]+1; c[v[i]]=i;
    }
//  for(int i=1;i<=n;++i){
//      for(int j=1;j<=i;++j) if(v[i]==v[j])
//          g[i]=max(g[i],g[j-1]+v[i]*(s[i]-s[j]+1)*(s[i]-s[j]+1));
//  }
//  for(int i=1;i<=n;++i) if(s[i]==1) q[v[i]].push_back(i);
    for(int i=1,j,k;i<=n;++i){
        k=v[i];
        if(q[k].size()<=1) q[k].push_back(i); else{
            for(int j=q[k].size()-1;j;--j)
                if(slp(q[k][j],q[k][j-1])<slp(q[k][j],i)) q[k].pop_back(); else break;
            q[k].push_back(i);
        }
        if(q[k].size()==1) j=q[k][0];
        else {
            int l=0,r=q[k].size()-1;
            for(int m;l<r;){
                m=l+r>>1;
                if(slp(q[k][m],q[k][m+1])>=s[i]) l=m+1;
                else r=m;
            }
            j=q[k][l];
        }
        f[i]=f[j-1]+v[i]*(s[i]-s[j]+1)*(s[i]-s[j]+1);
    }
    printf("%lld\n",f[n]);
}



D[玩具装箱]

题目难度从此开始下降
对于题目里面已经给出的式子,我们直接把它翻译成代码即可

f i = m i n { f j + ( g i g j L ) 2 }
这里 g i = s i + i , s i 表示前i个玩具长度之和
这里写一下转化式子的步骤,设k<j<i
f i = f j + ( g i g j L ) 2 < f k + ( g i g k L ) 2
f j f k < g k 2 g j 2 + 2 g i g j 2 g i g k 2 g j L + 2 g k + L
f j + g j 2 + 2 g j L f k g k 2 2 g k L < 2 ( g j g k ) g i
f j + g j 2 + 2 g j L f k g k 2 2 g k L 2 ( g j g k ) < g i

后面题目不再重复上述过程

#include<stdio.h>
#include<string.h>
#include<algorithm>
#define db double
#define N 50010
#define LL long long
using namespace std;
int n,L;
LL s[N],g[N],f[N],q[N];
inline db y(int j){
    return f[j]+g[j]*g[j]+2*g[j]*L;
}
inline db slp(int j,int k){ return (y(j)-y(k))/(2.*(g[j]-g[k])); }
int main(){
    scanf("%d%d",&n,&L); ++L;
    for(int i=1;i<=n;++i){  
        scanf("%lld",s+i);
        s[i]+=s[i-1]; g[i]=s[i]+i;
    }
    int l=0,r=0; f[0]=0;
    for(int i=1,j;i<=n;++i){
        while(l<r && slp(q[l],q[l+1])<=g[i]) ++l; j=q[l];
        f[i]=f[j]+(g[i]-g[j]-L)*(g[i]-g[j]-L);
        while(l<r && slp(q[r-1],q[r])>slp(q[r],i)) --r; q[++r]=i;
    }
    printf("%lld\n",f[n]);
}



E[仓库建设]

经典题目之一
这个题目我们考虑倒着做,因为直接计算距离不是非常方便
先计算出只建立一个仓库的代价之和S
f i 表示最后一个仓库放在i的最大节约代价
那么就可以得到一条非常简洁的转移式子

f i = m a x { f j + ( r j r i ) s i c i }

这里 r i 是第i个工厂到第n个工厂的距离, s i 是1~i个工厂的产品数量之和

#include<stdio.h>
#include<string.h>
#include<algorithm>
#define db double
#define N 1000010
#define LL long long
using namespace std;
int n,r[N],p[N],c[N],q[N];
LL s[N],f[N],S=0,g[N];
inline db slp(int j,int k){
    return (f[j]-f[k])/(db)(r[k]-r[j]);
}
int main(){
    scanf("%d",&n); 
    for(int i=1;i<=n;++i) scanf("%d%d%d",r+i,p+i,c+i),s[i]=s[i-1]+p[i];
    for(int i=1;i<n;++i) S+=(r[n]-r[i])*(LL)p[i]; S+=c[n];
    f[n]=0; int h=1,t=0; q[++t]=n;
    for(int i=n-1;i;--i){
        while(h<t && slp(q[h],q[h+1])>=s[i]) ++h;
        f[i]=f[q[h]]+(r[q[h]]-r[i])*s[i]-c[i];
        while(h<t && slp(q[t],q[t-1])<slp(q[t],i)) --t;
        q[++t]=i; *f=max(*f,f[i]);
    }
    printf("%lld\n",S-*f);
}   



F[Cats Transport]

终于来了几道CF的题
和上面一道题一样,我们依然先计算出只用一个feeder时的等待时间之和
让后再计算能优化多少,设 f i , j 表示用了i个feeder,最后一个在时刻j出发的答案
那么就得到了和上题几乎一模一样的式子

扫描二维码关注公众号,回复: 2871827 查看本文章
f i , j = m a x { f i 1 , k + ( r k r j ) s j }

注意用滚动数组就可以了,基本没有什么变化

#include<stdio.h>
#include<string.h>
#include<algorithm>
#define N 100010
#define db double
#define LL long long
using namespace std;
int n,m,k,q[N]; LL f[2][N],s[N],S,r[N],p[N],d[N],g[10][1000];;
inline db slp(int j,int k){ return (f[0][j]-f[0][k])/(r[k]-r[j]); }
int main(){
    scanf("%d%d%d",&n,&m,&k);
    for(int i=2;i<=n;++i) scanf("%lld",d+i),d[i]+=d[i-1];
    for(int x,i=1;i<=m;++i){
        scanf("%d%lld",&x,r+i); r[i]-=d[x];
    }
    sort(r+1,r+1+m); n=0; r[0]=-1000;
    for(int i=1;i<=m;++i){
        if(r[i]!=r[i-1]){ r[++n]=r[i]; p[n]=1; }
        else ++p[n];
    }
    for(int i=1;i<n;++i){ 
        S+=(r[n]-r[i])*(LL)p[i]; 
        s[i]=s[i-1]+p[i]; 
    }
    for(int T=1;T<k;++T){
        f[0][n]=0; int h=1,t=0; q[++t]=n;
        for(int i=n-1;i;--i){
            while(h<t && slp(q[h],q[h+1])>=s[i]) ++h;
            f[1][i]=f[0][q[h]]+(r[q[h]]-r[i])*s[i];
            while(h<t && slp(q[t],q[t-1])<slp(q[t],i)) --t;
            q[++t]=i;
        }
        swap(f[0],f[1]);
    }
    LL A=0;
    for(int i=1;i<=n;++i) A=max(A,f[0][i]);
    printf("%lld\n",S-A);
}



G[Product Sum]

也是一个比较简单的题
我们设 f i 表示移动i能获得最大的贡献
那么考虑把这个i移动到j这个位置,令 s i = j = 1 i a i 就得到转移式子

f i = m a x { s i s j + ( j i ) a i }

注意到这里并没有i<j的限制,所以我们要选择一项:
1.做两次
2.先求凸包再二分
肯定是第二项比较方便辣

#include<stdio.h>
#include<string.h>
#include<algorithm>
#define db double
#define LL long long
using namespace std;
int n,m,q[200010];
LL f[200010],v[200010],s[200010],S;
inline db slp(int j,int k){
    return (s[j]-s[k])/(j-k);
}
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;++i) scanf("%lld",v+i),s[i]=s[i-1]+v[i],S+=v[i]*i;;
    int t=0; q[++t]=0;
    for(int i=1;i<=n;++i){
        while(1<t && slp(q[t-1],q[t])>slp(q[t],i)) --t;
        q[++t]=i;
    }
    for(int i=1;i<=n;++i){
        int l=1,r=t;
        for(int m;l<r;){
            m=l+r>>1;
            if(slp(q[m],q[m+1])<v[i]) l=m+1;
            else r=m;
        }
        f[i]=s[i]-s[q[l]]+(q[l]-i)*v[i];
        *f=max(*f,f[i]);
    }
    printf("%lld\n",*f+S);
}



H[Levels and Regions]

这个题需要推一推式子,然而我期望这方面技巧极差
首先推一推打通第i关所需要的时间Ti,设i所在的Region为[l,r]
那么

T i = T i j = l i 1 t j j = l i t j + 1

p = t i j = l i t j 那么就得到 T i = ( 1 p ) T i + 1 就是 T i = 1 + p 1
那么整个Region的期望时间E(l,r)就是 E ( l , r ) = i = l r j = l i t j t i
为了方便dp,我们将这个式子转化一下,首先设 S i = j = 1 i t i
E ( l , r ) = i = l r S j S l 1 t i = i = l r S i t i S l 1 i = l r 1 t i

这里预处理一下 A i = j = 1 i S j t j , B i = j = 1 i 1 t j 就可以了
最后的dp式子是这样的:
f k , i = m i n { f k 1 , j + A i A j S j ( B i B j ) }
剩下的就是套路而已

#include<stdio.h>
#include<string.h>
#include<algorithm>
#define N 200010
#define db double
using namespace std;
int n,k,q[N];
db DP[2][N],AA[N],BB[N],s[N],v[N];
inline db y(int j){
    return DP[0][j]-AA[j]+s[j]*BB[j];
}
inline db slp(int j,int k){
    return (y(j)-y(k))/(s[j]-s[k]);
}
int main(){
    scanf("%d%d",&n,&k);
    for(int i=1;i<=n;++i){
        scanf("%lf",v+i);
        s[i]=s[i-1]+v[i];
        AA[i]=AA[i-1]+s[i]/v[i];
        BB[i]=BB[i-1]+1./v[i];
    }
    for(int i=1;i<=n;++i) DP[0][i]=AA[i];
    for(int T=2;T<=k;++T){
        int l=1,r=0; q[++r]=0;
        for(int i=1,j;i<=n;++i){
            while(l<r && slp(q[l],q[l+1])<BB[i]) ++l;
            j=q[l];
            DP[1][i]=DP[0][j]+AA[i]-AA[j]-s[j]*(BB[i]-BB[j]);
            while(l<r && slp(q[r-1],q[r])>=slp(q[r],i)) --r;
            q[++r]=i;
        }
        swap(DP[0],DP[1]);
    }
    printf("%.7lf\n",DP[0][n]);
}



I[序列分割]

回到Bzoj了
发现这个题,无论什么顺序分割序列,最终答案只和分割位置有关
那么计算一下发现就是每个块两两相乘,得到式子
f i , j 表示做到j,用了i块的最大结果
f i , j = m a x { f i 1 , k + ( s j s k ) s k } , s i = j = 1 i a i
滚动数组就没了

#include<stdio.h>
#include<string.h>
#include<algorithm>
#define db double
#define LL long long
using namespace std;
int n,k,q[100010]; LL f[2][100010],s[100010];
inline LL y(int j){ return f[0][j]-s[j]*s[j]; }
inline db slp(int j,int k){
    if(s[k]==s[j]) return 1e100;
    return (y(j)-y(k))/(s[k]-s[j]);
}
int main(){
    scanf("%d%d",&n,&k);
    for(int i=1;i<=n;++i) scanf("%lld",s+i),s[i]+=s[i-1];
    for(int i=1;i<=k;++i){
        int l=1,r=0; q[++r]=0;
        for(int j=1;j<=n;++j){
            while(l<r && slp(q[l],q[l+1])<s[j]) ++l;
            f[1][j]=f[0][q[l]]+(s[j]-s[q[l]])*s[q[l]];
            while(l<r && slp(q[r],q[r-1])>slp(q[r],j)) --r;
            q[++r]=j;
        }
        swap(f[0],f[1]);
    }
    printf("%lld\n",f[0][n]);
}



J[特别行动队]

前面那么多例题,这个题就不说了吧,注意a是负的就行了

#include<stdio.h>
#include<string.h>
#include<algorithm>
#define db double
#define LL long long
using namespace std;
int n,m,q[1000010],a,b,c;
LL f[1000010],v[1200010],s[1200010],S;
inline LL y(int j){ return f[j]+a*s[j]*s[j]-b*s[j]; }
inline db slp(int j,int k){
    return (y(j)-y(k))/(s[j]-s[k]);
}
int main(){
    scanf("%d%d%d%d",&n,&a,&b,&c);
    for(int i=1;i<=n;++i) scanf("%lld",v+i),s[i]=s[i-1]+v[i];
    int l=1,r=0; q[++r]=0;
    for(int i=1;i<=n;++i){
        while(l<r && slp(q[l],q[l+1])>2.*a*s[i]) ++l;
        int j=q[l];
        f[i]=f[j]+a*(s[i]-s[j])*(s[i]-s[j])+b*(s[i]-s[j])+c;
        while(l<r && slp(q[r-1],q[r])<slp(q[r],i)) --r; q[++r]=i;
    }
    printf("%lld\n",f[n]);
}

猜你喜欢

转载自blog.csdn.net/JacaJava/article/details/81612956