划分

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\),使得:

\[\sum_{i=1}^{k_1} a_i\le \sum_{i=k_1+1}^{k_2} a_i \le \dots \le \sum_{i=k_p+1}^n a_i \]

注意 \(p\) 可以为 \(0\) 且此时 \(k_0 = 0\),也就是小明可以将所有数据合并在一起运行。

小明希望他的程序在正确运行样例情况下,运行时间也能尽量小,也就是最小化

\[\left(\sum_{i=1}^{k_1} a_i \right)^2+\left(\sum_{i=k_1}^{k_2} a_i \right)^2+\cdots +\left(\sum_{i=k_p+1}^n a_i \right)^2 \]

小明觉得这个问题非常有趣,并向你请教:给定 \(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;
}

出题人的想法是把决策记下来最后算代价。