BZOJ4518: [Sdoi2016]征途
【传送门:BZOJ4518】
简要题意:
给出n个数,要求分成m个段,使得这些段的数和的方差最小,并将方差*m^2
题解:
斜率优化DP
我们设ans为最后的答案,sum表示所有的数之和,c[i]表示最优解中第i段数字和
下面式子中,1<=i<=m
ans=[∑(c[i]-sum/m)^2]/m*m^2
=[∑(c[i]-sum/m)^2]*m
=[∑(c[i]^2-2*c[i]*sum/m+sum^2/m^2)]*m
=∑(c[i]^2*m-2*c[i]*sum+sum^2/m)
=∑(c[i]^2*m-2*c[i]*sum)+sum^2
因为sum为所有数字和,所以sum=c[1]+...+c[m]
=∑(c[i]^2*m)+sum^2-2*sum^2
=m*∑(c[i]^2)-sum^2
因为m和sum^2的值是确定的,所以我们要求的是最小的每个段的平方和(也就是∑(c[i]^2))
设f数组,f[i][k]表示前i个数分成k个段的最小平方和
设s数组,s[i]表示1到i的数的和
那么我们很容易得到这样的方程:(1<=i<=n)
f[i][k]=min(f[j][k-1]+(s[i]-s[j])^2)
但是这样做的时间复杂度为O(n^2*m),绝对T
那么我们就用斜率优化来优化
以前都是一维的DP斜率优化,现在二维应该怎么做呢?(我一开始也不会。。)
从上面的DP方程,我们发现其实每次询问的时候都是只询问了分成当前减1的段的最优解(也就是f[i][k]询问f[j][k-1])
所以我们定义f1数组表示现在要分成k段要求的最小平方和,f2数组表示之前已经处理好的分成k-1段的最小平方和
这样我们就相当于用滚动的方法来处理,这样就能得到:
f1[i]=min(f2[j]+(s[i]-s[j])^2)
设j1<j2<i
就可以得到斜率不等式(f2[j2]-f2[j1]+s[j2]^2-s[j1]^2)/(s[j2]-s[j1])<2*s[i]
这道题就搞定了
注意一开始要先预处理f2,并且要从分成两段的情况开始
注意要开long long(不然可能会炸)
参考代码:
#include<cstdio> #include<cstring> #include<cstdlib> #include<cmath> #include<algorithm> using namespace std; typedef long long LL; LL a[3100],s[3100]; LL f1[3100],f2[3100]; int list[3100]; LL slop(int j1,int j2) { return (f2[j2]-f2[j1]+s[j2]*s[j2]-s[j1]*s[j1])/(s[j2]-s[j1]); } int main() { int n,m; scanf("%d%d",&n,&m); s[0]=0; LL sum=0; memset(f1,0,sizeof(f1)); memset(f2,0,sizeof(f2)); for(int i=1;i<=n;i++) { scanf("%lld",&a[i]); sum+=a[i]; s[i]=s[i-1]+a[i]; f2[i]=s[i]*s[i]; } for(int k=2;k<=m;k++) { int head=1,tail=1;list[head]=k-1; for(int i=k;i<=n;i++) { while(head<tail&&slop(list[head],list[head+1])<2*s[i]) head++; int j=list[head]; f1[i]=f2[j]+(s[i]-s[j])*(s[i]-s[j]); while(head<tail&&slop(list[tail-1],list[tail])>slop(list[tail],i)) tail--; list[++tail]=i; } for(int i=1;i<=n;i++) f2[i]=f1[i]; } printf("%lld\n",m*f1[n]-sum*sum); return 0; }