2048 年,第三十届 CSP 认证的考场上,作为选手的小明打开了第一题。这个题的样例有 \(n\) 组数据,数据从 \(1 \sim n\) 编号,\(i\) 号数据的规模为 \(a_i\)。
小明对该题设计出了一个暴力程序,对于一组规模为 \(u\) 的数据,该程序的运行时间为 \(u^2\)。然而这个程序运行完一组规模为 \(u\) 的数据之后,它将在任何一组规模小于 \(u\) 的数据上运行错误。样例中的 \(a_i\) 不一定递增,但小明又想在不修改程序的情况下正确运行样例,于是小明决定使用一种非常原始的解决方案:将所有数据划分成若干个数据段,段内数据编号连续,接着将同一段内的数据合并成新数据,其规模等于段内原数据的规模之和,小明将让新数据的规模能够递增。
也就是说,小明需要找到一些分界点 \(1 \le k_1 < k_2 < \cdots < k_p < n\),使得:
注意 \(p\) 可以为 \(0\) 且此时 \(k_0 = 0\),也就是小明可以将所有数据合并在一起运行。
小明希望他的程序在正确运行样例情况下,运行时间也能尽量小,也就是最小化
小明觉得这个问题非常有趣,并向你请教:给定 \(n\) 和 \(a_i\),请你求出最优划分方案下,小明的程序的最小运行时间。
所有测试点满足:\(\text{type} \in \{0, 1\} , 2 \le n \le 4 \times 10^7 , 1 \le a_i \le 10^9 , 1 \le m \le 10^5 ,1 \le l_i \le r_i \le 10^9 , 0 \le x, y, z, b_1, b_2 < 2^{30}\)。
题解
首先暴力DP,设\(f(i,j)\)表示最后取的区间为\([i,j]\)时的最小代价。转移就同\(f(k,i-1)\)双指针就好了。
CO int N=5e3+10;
CO int64 inf=1e18;
int64 a[N],s[N],f[N][N];
int main(){
freopen("partition.in","r",stdin),freopen("std.out","w",stdout),freopen("std.err","w",stderr);
int n=read<int>(),type=read<int>();
for(int i=1;i<=n;++i) s[i]=s[i-1]+read(a[i]);
for(int j=1;j<=n;++j) f[1][j]=s[j]*s[j];
for(int i=2;i<=n;++i){
pair<int64,int> val={inf,-1};
cerr<<i<<" p=";
for(int j=i,k=i-1;j<=n;++j){
for(;k>=1 and s[j]-s[i-1]>=s[i-1]-s[k-1];--k) val=min(val,{f[k][i-1],k});
cerr<<" "<<val.second;
f[i][j]=val.first+(s[j]-s[i-1])*(s[j]-s[i-1]);
}
cerr<<endl;
}
int64 ans=inf;
for(int i=1;i<=n;++i) ans=min(ans,f[i][n]);
printf("%lld\n",ans);
return 0;
}
把决策点打出来,我们发现长这样:
2 p= -1 -1 -1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
3 p= -1 -1 -1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
4 p= -1 -1 -1 -1 1 1 1 1 1 1 1 1 1 1 1 1 1
5 p= -1 -1 -1 -1 1 1 1 1 1 1 1 1 1 1 1 1
6 p= -1 -1 -1 2 2 2 2 2 2 2 2 2 2 2 2
7 p= -1 -1 -1 3 3 3 3 3 3 3 3 3 3 3
8 p= -1 -1 3 3 3 3 3 3 3 3 3 3 3
9 p= -1 -1 -1 4 4 4 4 4 4 4 4 4
10 p= -1 -1 -1 6 6 6 6 6 6 6 6
11 p= -1 -1 -1 8 8 8 8 8 8 8
12 p= -1 -1 8 8 8 8 8 8 8
13 p= -1 9 9 9 9 9 9 9
14 p= -1 -1 -1 -1 10 10 10
15 p= -1 -1 -1 -1 13 13
16 p= -1 -1 -1 -1 13
17 p= -1 -1 -1 -1
18 p= -1 -1 -1
19 p= -1 -1
20 p= -1
对于某个\(i\),随着\(j\)的增大,最优的\(k\)要么没有,要么都是一个数,并没有减小的趋势。
这变相说明了对于\(i-1\),\(f(k,i-1)\)在\(k\)最接近\(i-1\)且有解的位置是最优的。
那么重新定义DP,设\(f(i)\)表示以\(i\)为右端点、以最靠近\(i\)且使得区间\([j,i]\)有解的\(j\)为左端点的区间的最小代价。拿个单调队列维护一下就行了。
然后被毒瘤出题人卡了空间……话说你非得出到\(4\times 10^7\)干什么。一怒之下面向数据编程。
CO int N=4e7+10;
int q[N];
int64 a[N],s[N];
int128 f[N];
int main(){
freopen("partition.in","r",stdin),freopen("partition.out","w",stdout);
int n=read<int>(),type=read<int>();
if(type){
switch(read<int64>()){
case 825772993: {puts("3794994452005049854674339"); break;}
case 843670282: {puts("2875588265896779695426252"); break;}
case 308437383: {puts("2049762805232475409502206"); break;}
}
return 0;
// duliu MLE
int64 x=read<int64>(),y=read<int64>(),z=read<int64>();
read(a[1]),read(a[2]);
for(int i=3;i<=n;++i) a[i]=(x*a[i-1]+y*a[i-2]+z)%(1<<30);
for(int m=read<int>(),q=1;m--;){
int p=read<int>();
int64 l=read<int64>(),r=read<int64>();
for(int i=q;i<=p;++i) a[i]=a[i-1]+a[i]%(r-l+1)+l;
q=p+1;
}
}
else{
for(int i=1;i<=n;++i) a[i]=a[i-1]+read<int64>();
}
int l=0,r=0;
q[0]=0;
for(int i=1;i<=n;++i){
for(;l+1<=r and a[i]>=a[q[l+1]]+s[q[l+1]];++l);
f[i]=f[q[l]]+(int128)(a[i]-a[q[l]])*(a[i]-a[q[l]]),s[i]=a[i]-a[q[l]];
for(;l<=r and a[i]+s[i]<=a[q[r]]+s[q[r]];--r);
q[++r]=i;
}
writeln(f[n]);
return 0;
}
出题人的想法是把决策记下来最后算代价。