题面
题意
将一个序列 \(\{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; }
来源:https://www.cnblogs.com/Kilo-5723/p/12039123.html