BZOJ 1010 [玩具装箱 toy]

旧街凉风 提交于 2019-12-14 15:15:47

题面

题意

  将一个序列 \(\{a_n\}\) 分割成若干段,令第 \(i\) 段的的和为 \(s_i\),则代价为 \(\sum(s_i-L)^2\),求最小代价。

题解

  令 \(sum_i=\sum_{j=1}^ia_j\),用 \(dp_i\) 代表将前 \(i\) 项分割成若干段的最小代价,那么根据定义:

\[ \begin{aligned} dp_i&=\min_{j=0}^{i-1}\{dp_j+(sum_i-sum_j-L)^2\}\\ &=\min_{j=0}^{i-1}\{dp_j+sum_i^2-2sum_i(sum_j+L))+(sum_j+L)^2\}\\ &=sum_i^2+\min_{j=0}^{i-1}\{(-2sum_j+L)sum_i+(dp_j+(sum_j+L)^2)\}\\ \end{aligned} \]

  化成最后一行的形式之后,令 \(f_i(x)=(-2sum_i+L)x+(dp_i+(sum_i+L)^2)\),则 \(dp_i=sum_i^2+\min_{j=0}^{i-1}f_j(sum_i)\)。由于 \(f_i(x)\) 是一次函数,并且斜率 \(k_i<k_j(i<j)\),所以可以用队列维护一个由 \(l:f_i(x)\) 构成的上凸壳,每次先在上凸壳中查找 \(\min_{j=0}^{i-1}f_j(sum_i)\),再将 \(l:f_i(x)\) 加入凸壳中。

  由于每次询问的横坐标都递增,询问时可以弹出队列左端的直线直到其在 \(sum_i\) 处取得凸壳中的最小值。添加时将直线加入队列右端,不断弹出其左侧直线直到左侧直线没有被其在队列中的前后两条直线完全覆盖。


代码

#include<iostream>
#include<cstdio>
using namespace std;
typedef long long ll;
typedef long double ld;
const int maxn=5e4+5;
struct line{
    ll a,b;
    line(ll a=0,ll b=0):a(a),b(b){}
};
line que[maxn];
int l,r;
inline bool check(const line &a,const line &b,const line &c){
    return (ld)c.a*(b.b-a.b)+(ld)c.b*(a.a-b.a)<=
           (ld)a.a*(b.b-a.b)+(ld)a.b*(a.a-b.a);
}
void add(const line &a){
    while (r-l>1&&check(que[r-2],que[r-1],a))
        r--;
    que[r++]=a;
}
ll get(ll x){
    while (r-l>1&&(ld)que[l].a*x+que[l].b>=(ld)que[l+1].a*x+que[l+1].b)
        l++;
    return que[l].a*x+que[l].b;
}
line make(ll sum,ll dp,int l){
    return line(-2*(sum+l),dp+(sum+l)*(sum+l));
}
int main(){
    int i,n,l,t;
    ll sum,dp;
    scanf("%d%d",&n,&l);
    l++;
    sum=0; add(make(0,0,l));
    for (i=0;i<n;i++){
        scanf("%d",&t);
        sum+=t+1;
        dp=get(sum)+sum*sum;
        add(make(sum,dp,l));
    }
    printf("%lld\n",dp);
    return 0;
}
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!