划分
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),请你求出最优划分方案下,小明的程序的最小运行时间。
所有测试点满足:( ext{type} in {0, 1} , 2 le n le 4 imes 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
",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 imes 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;
}
出题人的想法是把决策记下来最后算代价。