[Luogu P4072 [SDOI2016]征途]题解
题面传送
Pine希望每一天走的路长度尽可能相近,所以他希望每一天走的路的长度的方差尽可能小。
提到了方差???
那么自然而然我们可以想到方差的公式[推导公式会按步骤给出]
\[s^2=\frac{\sum_{i=1}^{m}(v_i-\overline{v})^2}{m}
\]
\[\Rightarrow~~~~~~~~~~~~s^2=\frac{\sum_{i=1}^{m}~(v_i^2-2v_i\overline{v}+\overline{v}^2)}{m}
\]
\[\Rightarrow~~~~~ms^2=\sum_{i=1}^{m}v_i^2-2\overline{v}\sum_{i=1}^{m}v_i+\sum_{i=1}^{m}\overline{v}^2
\]
\[\because~~~~~\overline{v}=\frac{\sum_{i=1}^{m}v_i}{m}
\]
\[\therefore~~~~~ms^2=\sum_{i=1}^{m}v_i^2 - 2\frac{\sum_{i=1}^{m}v_i}{m}\sum_{i=1}^{m}v_i + \sum_{i=1}^{m}(\frac{\sum_{i=1}^{m}v_i}{m})^2
\]
\[又\because~~~~~\frac{\sum_{i=1}^{m}x}{m}=x
\]
\[\therefore~~~~~ms^2=\sum_{i=1}^{m}v_i^2 - 2\frac{(\sum_{i=1}^{m}v_i)^2}{m} + \frac{(\sum_{i=1}^{m}v_i)^2}{m}
\]
\[\therefore~~~~~m^2s^2=m\sum_{i=1}^{m}v_i^2 - (\sum_{i=1}^{m}v_i)^2
\]
我们不难发现,我们可以利用前缀和来计算出等式右边的结果
for(register int i=1;i<=n;++i){
read(s[i]);
s[i]+=s[i-1];
}
s[i]表示的是等式右边第二项
\[f_{x,y}表示走了前x段一共花了y天, 最小的\sum_{i=1}^{x}v_i^2,转移方程很容易得出为:
\]
\[f_{x,y}=\min{( f_{x,y} , f_{k,y-1}+(s[x]-s[k])^2 ) }
\]
\[根据m^2s^2=m\sum_{i=1}^{m}v_i^2 - (\sum_{i=1}^{m}v_i)^2
\]
\[最后面的结果m^2s_n^2自然就是m*f_{n,m}-s_n*s_n
\]
即可写出代码:
#include<bits/stdc++.h>
using namespace std;
#define N 3005
int f[N][N],s[N];
int n,m;
inline int read(){
int w=0,sum=0;
char ch=getchar();
while(!isdigit(ch)){
w|=(ch=='-');
ch=getchar();
}
while(isdigit(ch)){
sum=(sum<<1)+(sum<<3)+(ch^48);
ch=getchar();
}
return w?-sum:sum;
}
signed main(){
memset(f,0x3f,sizeof(f));f[0][0]=0;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i){
scanf("%d",&s[i]);
s[i]+=s[i-1];
}
for(int j=1;j<=m;++j)
for(int i=1;i<=n;++i)
for(int k=0;k<i;++k)
f[i][j]=min(f[k][j-1]+(s[i]-s[k])*(s[i]-s[k]),f[i][j]);
printf("%d\n",m*f[n][m]-s[n]*s[n]);
return 0;
}
写到这一步「在luogu上」已经有90分的高分了,不过还是AC不了这道题,那么我们如何进一步优化呢?
需要用到斜率优化,在本篇博客中只大概提到一点,若想了解更多或没有前置知识请移步一位dalao的blog
怎么优化呢?有两种方法[可谓是殊途同归]
回到状态:
\[f_{x,y}=\min{( f_{x,y} , f_{k,y-1}+(s[x]-s[k])^2 ) }
\]
我们可以化成
\[f_{x,y}=f_{k,y-1}+(s[x]-s[k])^2
\]
\[\Rightarrow~~~~~~f_{x,y}=f_{k,y-1}+s[x]^2-2s[x]*s[k]+s[k]^2
\]
\[\Rightarrow~~~~~~f_{k,y-1}+s[k]^2=2s[x]*s[k]+f_{x,y}-s[x]^2
\]
\[有一次函数y=kx+b
\]
\[我们可以将转移式中的值看成函数中的量
\]
\[ \left\{
\begin{aligned}
f_{k,y-1}+s[k]^2 \Rightarrow y \\
2s[x] \Rightarrow k \\
s[k] \Rightarrow x \\
f_{x,y}-s[x]^2 \Rightarrow b \\
\end{aligned}
\right. \]
\[根据斜率计算式~~~~~k=\frac{y_1-y_2}{x_1-x_2}可得
\]
\[那么若j比k更优,则\frac{y_j-y_k}{x_j-x_k}<2s_x
\]
\[也就是\frac{f_{j,y-1}+s[j]^2-f_{k,y-1}-s[k]^2}{s[j]-s[k]}<2s_x
\]
又一次回到状态:
\[f_{x,y}=\min{( f_{x,y} , f_{k,y-1}+(s[x]-s[k])^2 ) }
\]
若j比k更优使f[x]更新后更小那么
\[f_{j,y-1}+(s[x]-s[j])^2 < f_{k,y-1}+(s[x]-s[k])^2
\]
\[\Leftrightarrow \frac{f_{j,y-1}+s[j]^2-f_{k,y-1}-s[k]^2}{s[j]-s[k]}<2s_x
\]
\[和第一种的一样
\]
有了上述结论,我们就可以很好的用单调队列来维护一个凸壳了
while(head<tail&&slope(q[head],q[head+1])<2*s[i]) //我们上述讨论的第一、二种情况就是这个用的
++head;
while(head<tail&&slope(q[tail-1],q[tail])>slope(q[tail-1],i))// 如果加入后发现前面的那个点凹进去了就弹出前面那个点
--tail;
q[++tail]=i;
\[Slope函数
\]
\[按照上面的\frac{f_{j,y-1}+s[j]^2-f_{k,y-1}-s[k]^2}{s[j]-s[k]}就行了
\]
各位看官在状态转移时是不是看着二维数组有些心烦,其实我们可以用滚动数组[或者两个数组优化]
f[i]=g[q[head]]+(s[i]-s[q[head]])*(s[i]-s[q[head]]);
\[g数组就是保存的(\sum_{i=1}^{m}v_i)^2
\]
最后的最后附上完整Code
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define N 5005
int n,m,q[N];
ll s[N];
ll f[N],g[N],head=1,tail=1;
inline double X(int x){
return 1.0*s[x];
}
inline double Y(int x){
return 1.0*g[x]+1.0*s[x]*s[x];
}
inline double slope(int i,int j){
return (Y(i)-Y(j))/(X(i)-X(j));
}
signed main(){
scanf("%d%d",&n,&m);
for(register int i=1;i<=n;++i){
scanf("%d",&s[i]);
s[i]+=s[i-1];
g[i]=s[i]*s[i];
}
for(register int l=1;l<m;++l){
head=tail=1;
q[head]=l;
for(register int i=l+1;i<=n;++i){
while(head<tail&&slope(q[head],q[head+1])<2*s[i])
++head;
f[i]=g[q[head]]+(s[i]-s[q[head]])*(s[i]-s[q[head]]);
while(head<tail&&slope(q[tail-1],q[tail])>slope(q[tail-1],i))
--tail;
q[++tail]=i;
}
for(register int i=1;i<=n;++i)
g[i]=f[i];
}
printf("%lld\n",m*f[n]-s[n]*s[n]);
return 0;
}

浙公网安备 33010602011771号