[NOIP2021]方差

题目

传送门 to luogu

思路

受到了最小方差生成树的启发,并且利用这个可以很容易得到结论,于是便忘记了,也许一个方法只应用于证明而不用于代码实现。

我自始至终都在想着,枚举所谓的平均值 a ˉ \bar a aˉ 。枚举之后,需要考虑数组变化的内在规律是什么?画个图,就会忽然意识到,本质上等价于 对差分数组重排。那么比 a ˉ \bar a aˉ 小的部分,为了尽可能靠近 a ˉ \bar a aˉ,斜率是单减的;比 a ˉ \bar a aˉ 大的部分,斜率则是单增的。但是,满足 a i < a ˉ < a i + 1 a_i<\bar a<a_{i+1} ai<aˉ<ai+1 的这个差分值 ( a i + 1 − a i ) (a_{i+1}-a_i) (ai+1ai) 好像就说不准了?

事实上,这个差分值要么比前一个小,要么比后一个小。反证法。譬如 a ˉ ⩾ a i + a i + 1 2 \bar a\geqslant\frac{a_i+a_{i+1}}{2} aˉ2ai+ai+1,那么此时对 a i a_i ai 进行操作,辄变为数轴上关于 a i − 1 + a i + 1 2 \frac{a_{i-1}+a_{i+1}}{2} 2ai1+ai+1 的对称。由反证的假设, a i − a i − 1 < a i + 1 − a i a_i-a_{i-1}<a_{i+1}-a_i aiai1<ai+1ai a i − 1 + a i + 1 2 > a i {a_{i-1}+a_{i+1}\over 2}>a_i 2ai1+ai+1>ai,又有 a i − 1 + a i + 1 2 ⩽ a i + a i + 1 2 ⩽ a ˉ {a_{i-1}+a_{i+1}\over 2}\leqslant{a_i+a_{i+1}\over 2}\leqslant \bar a 2ai1+ai+12ai+ai+1aˉ,所以对称之后 a i a_i ai 一定更靠近 a ˉ \bar a aˉ,至少不会更劣;而对 a i a_i ai 进行操作的时候,就把这个差分值向前移动了一步,会一直移动到最前面。而 a ˉ < a i + a i + 1 2 \bar a<{a_i+a_{i+1}\over 2} aˉ<2ai+ai+1 就操作 a i + 1 a_{i+1} ai+1 呗,同理。

所以,一定存在一个分界点,使得前面的差分值单减、后面的差分值单增。但是此时我仍然没有摆脱枚举 a ˉ \bar a aˉ 这个根深蒂固的想法……用 O ( n a ) \mathcal O(na) O(na) 枚举 a ˉ \bar a aˉ 之后,将差分值从大到小排序,从两边往中间填,状态为某一边的末尾 a a a 值;这样就是 O ( n 2 a 2 ) \mathcal O(n^2a^2) O(n2a2) 的了。

最小方差生成树中,边的权值之间非常不独立,让我们难以运用贪心求解最小生成树,所以我们才枚举 a ˉ \bar a aˉ 。在这道题里,其实完全不需要枚举 a ˉ \bar a aˉ 了!化简一下 n 2 D n^2D n2D 的式子得到
n 2 D = n ∑ a i 2 − ( ∑ a i ) 2 n^2D=n\sum a_i^2-\left(\sum a_i\right)^2 n2D=nai2(ai)2
可以直接对它进行动态规划。从两边往中间放行不通了,考虑从中间往两边放。这相当于一个动态变化的过程,可以在前面插入差分值(将所有数增大这个差分值)或者在最后插入。奇特的是,将所有数增大 d d d ∑ a i 2 \sum a_i^2 ai2 的影响可以用 ∑ a i \sum a_i ai 轻易求出!

于是状态中存储 ∑ a i \sum a_i ai 即可。这样的复杂度是 O ( n 2 a ) \mathcal O(n^2a) O(n2a) 的,无法通过最后的测试点。

有哪里可以改进吗?剪枝呗!求出当前最大可行的 ∑ a i \sum a_i ai 是多少,然后严格在范围内转移。这样就可以通过了,时间复杂度 O ( n a 2 ) \mathcal O(na^2) O(na2) 。为啥呢?因为差分数组的总和只有 a a a,所以一定有大量的 0 0 0 存在。它们就不会提供转移复杂度了。所以,这个剪枝看上去是剪背包容量,实际上是剪物品个数。

代码

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cctype>
#include <climits>
using namespace std;
# define rep(i,a,b) for(int i=(a); i<=(b); ++i)
# define drep(i,a,b) for(int i=(a); i>=(b); --i)
typedef long long llong;
inline int readint(){
    
    
	int a = 0, c = getchar(), f = 1;
	for(; !isdigit(c); c=getchar())
		if(c == '-') f = -f;
	for(; isdigit(c); c=getchar())
		a = (a<<3)+(a<<1)+(c^48);
	return a*f;
}
void writeint(unsigned x){
    
    
	if(x > 9) writeint(x/10);
	putchar(char((x%10)^48));
}
inline void getMin(llong &x,const llong &y){
    
    
	if(y < x) x = y;
}

const int MAXN = 500005;
const long long INFTY = LONG_LONG_MAX>>1;
long long dp[2][MAXN];
int a[MAXN];

int main(){
    
    
	int n = readint(), s = readint();
	for(int i=1,nxt; i!=n; ++i){
    
    
		nxt = readint();
		a[i] = nxt-s, s = nxt;
	}
	sort(a+1,a+n,less<int>()); s = 0;
	fill(dp[0]+1,dp[0]+(MAXN<<1),INFTY);
	for(int i=1,fr=0,p=0; i!=n; ++i,fr^=1){
    
    
		s += a[i]; // current endpoint
		const int mov = i*a[i]; // append in front
		fill(dp[i&1],dp[i&1]+p+mov+1,INFTY);
		llong gap = llong(n)*s*s;
		rep(j,0,p) getMin(dp[i&1][j+s],dp[fr][j]+gap);
		gap = llong(i)*n*a[i]*a[i];
		const int step = n*(a[i]<<1); // for every j
		for(int j=0; j<=p; ++j,gap+=step)
			getMin(dp[i&1][j+mov],dp[fr][j]+gap);
		p += mov; // new range of sum
	}
	llong ans = INFTY;
	const int id = (n&1)^1; // last shot
	rep(i,0,s*(n-1))
		getMin(ans,dp[id][i]-llong(i)*i);
	printf("%lld\n",ans);
	return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_42101694/article/details/122021201