[SDOI2016]征途(斜率优化)
在一个风雨如晦的晚上, q0000000 同学被一道紫题切爆了。
一、 关于思路
q0000000 是奔着斜率优化来刷题的,如果他在考场上遇到这道题,他就得先推完式子才知道了。
- 路的长度和天数,这似乎是两个可以用的状态;
- 题目里出现了方差这种奇怪的东西,应该从这里开始推式子;
- 题目给出了 \(v \times m^2\) 的输出形式,推式子的时候显然要把它乘上——为什么一开始没乘呢?
q0000000 想要先推一个暴力式子,然后优化它。
二、 关于式子
从方差的定义出发 \(s^2=\frac{1}{m}\sum_{i=1}^{n}{(x_i - \overline{x})^2}\) .
乘上 \(m^2\) ,得 \(m\sum_{i=1}^{m}{{x_i}^2}-\sum_{i=1}^{m}{{x_i}^2}\) .
不难发现,后半部分是一个常量, \(m\) 也可以最后一起乘,真正要关注的只有 \(\sum_{i=1}^{m}{{x_i}^2}\) 。dp数组记录的也就是它——最小平方和,这可以用前缀和处理一下。
定义 \(f_{k,i}\) 为前 \(i\) 段路走了 \(k\) 天,从第一天到现在,每天走的路径长度之和的最小平方和。
dp式子得到了,\(f_{k,i}=min\{f_{k-1,j}+(s_i-s_j)^2\}\)。式子中的 \(s_i-s_j\) 表示第 \(k\) 天走的距离,这一天走了第 \(j+1\) 到 \(i\) 段路,把它加进整体的最小平方和。时间复杂度为 \(O(n^2m)\)
转化为斜率优化形式反而较为简单, \(f_j+{s_j}^2=2\times s_i\times s_j+(f_i-{s_i}^2)\) 。 \(y,x,k,b\) 不必赘述。时间复杂度 \(O(nm)\) 。
三、 关于爆零
- 初始化 f 数组,第一天走任意远,否则后面会少情况。求前缀和顺便
f[1][i]=s[i]*s[i]
,枚举天数从 2 开始。 - 回看暴力思路,枚举天数,枚举这一天走到哪,枚举昨天走到哪,“每一天都是一个全新开始”,注意清空队列。
for(int k=2;k<=m;++k){ int l=1,r=1; //枚举i }
- 其他斜率优化细节。
四、 关于代码
#include<stdio.h>
typedef long double db;
typedef long long ll;
const int N=3000+10;
int n,m;
ll s[N],f[N][N],q[N];
inline ll rd(){
ll x=0;int f=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-') f^=1;c=getchar();}
while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c^48);c=getchar();}
return f?x:-x;
}
inline db k(int p,ll a,ll b){
db Y=(f[p][a]+s[a]*s[a])-(f[p][b]+s[b]*s[b]);
db X=s[a]-s[b];
return Y/X;
}
int main(){
n=rd(),m=rd();
for(int i=1;i<=n;++i){
s[i]=rd();
s[i]+=s[i-1],f[1][i]=s[i]*s[i];
}
for(int j=2;j<=m;++j){
int l=1,r=1;
for(int i=1;i<=n;++i){
while(l<r&&k(j-1,q[l],q[l+1])<=2*s[i]) ++l;
f[j][i]=f[j-1][q[l]]+(s[i]-s[q[l]])*(s[i]-s[q[l]]);
while(l<r&&k(j-1,q[r-1],q[r])>=k(j-1,i,q[r])) --r;
q[++r]=i;
}
}
printf("%lld\n",m*f[m][n]-s[n]*s[n]);
return 0;
}