【NOIP2021 方差】题解

题目链接

Part A 式子化简

首先题目要求的式子就是 n2 乘上 1ni=1n(aia¯)2,其中 a¯=1ni=1nai

我们把这三合在一起也就是:

n2×1ni=1n(ai1nj=1naj)2

前面化简一下,后面用完全平方公式展开:

n×i=1n(ai22×(1nj=1naj)×ai+1n2(j=1naj)2)

拆开:

n×i=1nai2n×i=1n(2×1n(j=1naj)×ai)+n×i=1n(1n2(j=1naj)2)

对于第二坨,把与 ai 无关的抽出来:

n×i=1nai2n×2×1n(j=1naj)×i=1nai+n×i=1n(1n2(j=1naj)2)

把第二坨化简一下:

n×i=1nai22×i=1nai×i=1nai+n×i=1n(1n2(j=1naj)2)

把第二坨的后两个写成平方的形式:

n×i=1nai22×(i=1nai)2+n×i=1n(1n2(j=1naj)2)

看第三坨,发现没有和 i 有关的项,之间把 i=1n 变成乘 n

n×i=1nai22×(i=1nai)2+n×n×1n2(j=1naj)2

化简一下第三坨:

n×i=1nai22×(i=1nai)2+(i=1nai)2

然后我们发现二、三坨可以合并:

n×(i=1nai2)(i=1nai)2

于是对于每种 a 我们都有直接算的方法了。

Part B 差分交换

现在让我们考虑令一个问题。

对于相邻的三个数 ai1aiai+1,我们计算相邻两数的差分别为 aiai1ai+1ai

先在我们把 ai 变成 ai1+ai+1ai,现在就是 ai1ai1+ai+1aiai+1

然后我们再对相邻两数作差分别为:ai+1aiaiai1

发现了什么?

没错,我们对每个数进行交换,只不过是把相邻两项的差交换

于是我们对 a 作差分序列 d,这样无论我们对 a 序列进行怎样的变换,d 序列的元素始终不变,只是顺序改变

基于这个结论,我们可以枚举 d 的顺序,时间复杂度 O(n!)

Part C 差分单谷性

这里我们可以引出一个结论:d 的排列必然是先从大到小,再从小到大。

这里可以感性理解一下,因为对于每个 a 只有不断靠近平均数,方差才最小。

我们也可以大致运用反证法证明。(有误请指出)

看不到?戳这里查看图片

图中 d1>d2,我们发现 a1a3 的值不变,图2相比图1中只有 a2 变大了。

两个 a2 相比,显然图1中的 a2 更靠近谷底,也就是 a¯。按照开始的式子 1ni=1n(aia¯)2 我们发现 a2 取图1的情况更优。

有了这个结论,我们可以把 d 按从大到小排序,对于 di,要么放左边,要么放右边,通过此方法,我们就得到了一种 O(2n) 的暴力。

48分Code
// Problem: P7962 [NOIP2021] 方差【民间数据】
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P7962
// Memory Limit: 512 MB
// Time Limit: 1000 ms
// 
// Powered by CP Editor (https://cpeditor.org)

#include<bits/stdc++.h>
using namespace std;
// #define int long long
inline int read(){int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;
ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+
(x<<3)+(ch^48);ch=getchar();}return x*f;}
#define N 410
//#define mo
int n, m, i, j, k; 
int a[N], d[N], b[N]; 
int ans=0x7fffffff; 
int sum, num; 

bool cmp(int x, int y)
{
	return x>y; 
}

void dfs(int x, int l, int r, int s, int t)
{
	if(l==r-1)
	{
		ans=min(ans, n*t-s*s);
		return ; 
	}
	int k; 
	k=a[l+1]=a[l]+d[x]; dfs(x+1, l+1, r, s+k, t+k*k); 
	k=a[r-1]=a[r]-d[x]; dfs(x+1, l, r-1, s+k, t+k*k); 
}

signed main()
{
//	freopen("tiaoshi.in","r",stdin);
//	freopen("tiaoshi.out","w",stdout);
	n=read(); 
	for(i=1; i<=n; ++i) a[i]=read(); 
	for(i=2; i<=n; ++i) d[i-1]=a[i]-a[i-1];  
	sort(d+1, d+n, cmp); 
	dfs(1, 1, n, a[1]+a[n], a[1]*a[1]+a[n]*a[n]); 
	printf("%d", ans); 
	return 0;
}

Part D 相对位置代替绝对位置

可以发现,我们在上面的方法中每次都把 a 的绝对位置求出来。但其实我们可以只求出其相对位置。

我们先对 d 从小到达排序,再一条一条放进去。此时 ai 的位置是在这种放进去的方法下的 j=1idj

首先让我们把之前的式子搬回出来:

n×(i=1nai2(i=1nai)2

我们要让结果最小,就是要让 i=1nai 最大,i=1nai2 最小。

让我们设 f(i,j) 表示已经决定的 ia 里面,和为 j 的最小平方和。设 S 为当前的 ij=1idi

j 可以理解为 i=1naif(i,j) 可以理解为 i=1nai2

现在对于 di+1,要么放在左边,要么放在右边。

先说放在右边,j 则会加上当前点的坐标,答案加上当前的平方,而当前点为 S+di

f(i+1,j+(S+di))=f(i,j)+S×S

放在左边的话,就是整体 a 右移 di+1j 就明显是加上 (i+1)×di+1,至于 f 的话,这里要手推一下。

首先至少要加上这个点 di2,然后后面的和变成 k=1i(ak+di)2,而原先是 k=1iak2,所以加上的是:

k=1i(ak+di)2k=1iak2

前面用完全平方公式拆一下:

k=1iak2+k=1i(2×ak×di)+k=1idi2k=1iak2

最前面与最后面两坨消去:

k=1i(2×ak×di)+k=1idi2

前面那坨把无关紧要的提出了:

2×di×k=1ak+k=1idi2

j 套进去:

2×di×j+k=1idi2

后面那坨直接把 k=1i 变成乘 i

2×di×j+i×di2

根据这个,我们可以把放左边的dp推出了:

f(i+1,j+di+1×(i+1))=f(i,j)+2×di×j+i×di2+di2

后面两个合一下:

f(i+1,j+di+1×(i+1))=f(i,j)+2×di×j+(i+1)×di2

然后就行了。

转移方程与最终代码可能有一些细节的东西,改一改就行了。

可是按照这个方法打只有72分。

72分code
// Problem: P7962 [NOIP2021] 方差【民间数据】
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P7962
// Memory Limit: 512 MB
// Time Limit: 1000 ms
// 
// Powered by CP Editor (https://cpeditor.org)

#include<bits/stdc++.h>
using namespace std;
#define int long long
inline int read(){int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;
ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+
(x<<3)+(ch^48);ch=getchar();}return x*f;}
#define N 410
#define M 610
//#define mo
int n, m, i, j, k; 
int a[N], d[N]; 
int ans=0x7fffffffff; 
int sum, num; 
int f[N][N*M]; 

signed main()
{
//	freopen("tiaoshi.in","r",stdin);
//	freopen("tiaoshi.out","w",stdout);
	n=read(); 
	for(i=1; i<=n; ++i) a[i]=read(); 
	for(i=2; i<=n; ++i) d[i-1]=a[i]-a[i-1];  
	sort(d+1, d+n); 
	for(i=0; i<=n; ++i) for(j=0; j<=240000; ++j) f[i][j]=ans; 
	f[0][0]=0; 
	for(i=0; i<n-1; ++i)
	{
		sum+=d[i+1]; 
		for(j=0; j<=240000; ++j)
		{
			f[i+1][j+sum]=min(f[i+1][j+sum], f[i][j]+sum*sum); 
			f[i+1][j+d[i+1]*(i+1)]=min(f[i+1][j+d[i+1]*(i+1)], f[i][j]+2*j*d[i+1]+(i+1)*d[i+1]*d[i+1]); 
			// if(f[i][j]!=ans)
			// printf("f[%lld][%lld]=%lld\n", i, j, f[i][j]); 
		}
	}
		
	for(i=0; i<=24000; ++i) 
	{
		// if(f[n-1][i]!=0x7fffffffff) printf("f[%lld][%lld]=%lld\n", n-1, i, f[n-1][i]); 
		// ans=min(ans, n*(f[n-1][i]+2*i*a[1]+(n-1)*a[1]*a[1]+a[1]*a[1])-(a[1]*n+i)*(a[1]*n+i));  
		ans=min(ans, n*f[n-1][i]-i*i); 
	}
		
	printf("%lld", ans); 
	return 0;
}

Part E 小优化

我们发现主要是集中在MLE和TLE。

MLE方面,我们可以使用滚动数组。

TLE的话,对于 j 的循环范围,我们可以动态分配。

提一句,最后加不加上 a1 都行(实测可以)。

最后贴上AC code:

// Problem: P7962 [NOIP2021] 方差【民间数据】
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P7962
// Memory Limit: 512 MB
// Time Limit: 1000 ms
// 
// Powered by CP Editor (https://cpeditor.org)

#include<bits/stdc++.h>
using namespace std;
#define int long long
inline int read(){int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;
ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+
(x<<3)+(ch^48);ch=getchar();}return x*f;}
#define N 10010
#define M 610
//#define mo
int n, m, i, j, k; 
int a[N], d[N]; 
int ans=0x7ffffffffff; 
int sum, num, p, q; 
int f[2][500010]; 

signed main()
{
//	freopen("tiaoshi.in","r",stdin);
//	freopen("tiaoshi.out","w",stdout);
	n=read(); 
	for(i=1; i<=n; ++i) a[i]=read(); 
	for(i=2; i<=n; ++i) d[i-1]=a[i]-a[i-1];  
	sort(d+1, d+n); 
	for(j=0; j<=500000; ++j) f[1][j]=f[0][j]=ans; 
	f[0][0]=0; 
	for(i=0; i<n-1; ++i)
	{
		sum+=d[i+1]; 
		p=(i+1)%2; q=i%2; num+=d[i+1]*(i+1); 
		for(j=0; j<=num; ++j) f[p][j]=ans; 
		for(j=0; j<=num; ++j)
		{
			if(j+sum<=num)
				f[p][j+sum]=min(f[p][j+sum], f[q][j]+sum*sum); 
			if(j+d[i+1]*(i+1)<=num)
				f[p][j+d[i+1]*(i+1)]=min(f[p][j+d[i+1]*(i+1)], f[q][j]+2*j*d[i+1]+(i+1)*d[i+1]*d[i+1]); 
		}
	}
		
	for(i=0; i<=500000; ++i) 
	{
		//ans=min(ans, n*(f[(n-1)%2][i]+2*i*a[1]+(n-1)*a[1]*a[1]+a[1]*a[1])-(a[1]*n+i)*(a[1]*n+i));  
//可写可不写,是是否加上a1的情况
		ans=min(ans, n*f[n-1][i]-i*i); 
	}
		
	printf("%lld", ans); 
	return 0;
}
posted @   zhangtingxi  阅读(780)  评论(1编辑  收藏  举报
编辑推荐:
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示