【NOIP2021】方差(动态规划、贪心+搜索)

22 篇文章 0 订阅
14 篇文章 0 订阅

题面

🔗

🔗

🔗

具体题面就不拉了吧,这题应该挺有名的。

题解

首先有一个变换大家必须注意到,这个数组的差分数组可以任意重排。

然后,对它找找规律,不难发现,重排后的差分数组,大致是先递减后递增的,中间一堆 0,这是因为我们求的是最小方差,而方差是描述数据离散程度的量,

一定存在一个分界点,使得前面的差分值单减、后面的差分值单增。但是此时我仍然没有摆脱枚举 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 存在。它们就不会提供转移复杂度了。所以,这个剪枝看上去是剪背包容量,实际上是剪物品个数。
……
原文链接

我们要做的是让所有数尽量靠近平均值,那么这个平均值就和上文的“分界点”有很大关联,由于差分数组的非零元素最多 600 个,所以可以除去非零元素后进行动态规划,时间复杂度 O ( n a 2 ) O(na^2) O(na2)


太 低 级 了 !

接下来介绍一个 O ( n a ) O(na) O(na) 的可过做法,贪心+搜索。

由于平均值前面的差分值单减,后面的差分值单增,所以我们若知道了平均值,就可以从大到小枚举差分值,然后考虑往左还是往右填。

这里可以用贪心证明一个结论:每次一定先放入距离平均值更远的一边,前提是放完后不越过平均值线

我们假设某时刻左边到平均值还有 L L L 的空间,右边还有 R R R 的空间,此时有两个差分值 a , b a,b a,b 要放入( b < a < L < R b<a<L<R b<a<L<R),那么比较 a a a 放两边的结果,就会发现填入右边更优。详细推导略。

我们会发现这个填数过程中, L L L 的小数部分、 R R R 的小数部分都不变,那么,若比较两数大小,到小数部分时的结果都是固定的。那么我们就可以优化枚举,不再枚举 n a na na 个平均数,而是枚举所有的 N N N N + 1 3 N+\frac{1}{3} N+31 N + 2 3 N+\frac{2}{3} N+32 N ∈ { 0 , 1 , 2 , . . . , a m a x } N\in\{0,1,2,...,a_{max}\} N{0,1,2,...,amax}),一共 O ( a ) O(a) O(a) 个。

但是最终总得触及平均值线,怎么办?我们就暴力枚举最小的几位(两三个就够了)非零差分值该放哪边,这样并不影响 O ( n a ) O(na) O(na) 的时间复杂度。

CODE

#include<map>
#include<set>
#include<queue>
#include<vector>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
#define MAXN 10005
#define LL long long
#define ENDL putchar('\n')
#define DB double
#define lowbit(x) (-(x) & (x))
#define FI first
#define SE second
LL read() {
	LL f = 1,x = 0;int s = getchar();
	while(s < '0' || s > '9') {if(s<0)return -1;if(s=='-')f=-f;s = getchar();}
	while(s >= '0' && s <= '9') {x = (x<<1) + (x<<3) + (s^48);s = getchar();}
	return f*x;
}
void putpos(LL x) {if(!x)return ;putpos(x/10);putchar((x%10)^48);}
void putnum(LL x) {
	if(!x) {putchar('0');return ;}
	if(x<0) putchar('-'),x = -x;
	return putpos(x);
}
void AIput(LL x,int c) {putnum(x);putchar(c);}

int n,m,s,o,k;
int a[MAXN],b[MAXN];
int main() {
	freopen("variance.in","r",stdin);
	freopen("variance.out","w",stdout);
	n = read();
	for(int i = 1;i <= n;i ++) {
		a[i] = read();
		if(i > 1) b[i-1] = a[i]-a[i-1];
	}
	sort(b + 1,b + n);
	int cc = 0;
	for(int i = 1;i < n;i ++) if(b[i] == 0) cc ++;
	LL ans = 1e18;
	for(int A = a[1]*3;A <= a[n]*3;A ++) {
		for(int B = 0;B < 4;B ++) {
			LL cna = a[1]+a[n],cn2 = a[1]*a[1] + a[n]*a[n];
			DB av = A/3.0;
			DB ct1 = av-a[1],ct2 = a[n]-av;
			int c1 = a[1],c2 = a[n];
			for(int i = n-1;i > cc+3;i --) {
				if(ct1 > ct2) {
					c1 += b[i]; ct1 -= b[i];
					cna += c1; cn2 += c1*c1;
				}
				else {
					c2 -= b[i]; ct2 -= b[i];
					cna += c2; cn2 += c2*c2;
				}
			}
			for(int i = min(n-1,cc+3);i > cc+1;i --) {
				if(B & (1<<(i-cc-2))) {
					c1 += b[i]; ct1 -= b[i];
					cna += c1; cn2 += c1*c1;
				}
				else {
					c2 -= b[i]; ct2 -= b[i];
					cna += c2; cn2 += c2*c2;
				}
			}
			for(int i = cc+1;i > 1;i --) {
				if(ct1 > ct2) {
					c1 += b[i]; ct1 -= b[i];
					cna += c1; cn2 += c1*c1;
				}
				else {
					c2 -= b[i]; ct2 -= b[i];
					cna += c2; cn2 += c2*c2;
				}
			}
//			printf("cna: %lld   cn2: %lld\n",cna,cn2);
			ans = min(ans,cn2*n - cna*cna);
		}
	}
	AIput(ans,'\n');
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值