题解:P10455 Genius Acm

更易于理解的题面

给定一个整数 \(M\) ,对于任意一个整数集合 \(S\),定义“校验值”如下:

从集合 \(S\) 中取出 \(M\) 对数(即 \(2M\) 个数,不能重复使用集合中的数,如果 \(S\) 中的整数不够 \(M\) 对,则取到不能取为止),使得“每对数的差的平方之和”最大,这个最大值就称为集合 \(S\) 的“校验值”。

现在给定一个长度为 \(N\) 的数列 \(A\) 以及一个整数 \(K\)

我们要把 \(A\) 分成若干段,使得每一段的“校验值”都不超过 \(K\)

求最少需要分成几段。

“校验值”

考虑贪心,将第 \(k\) 大的值与第 \(k\) 小的值组合 (这个 \(k\) 不是题面中的那个 \(k\),下同) 即可使“每对数的差的平方之和”最大。

可以将数组排序后双指针求出,求一次“校验值”的时间复杂度为 \(O(N \log N)\)

对于一组已经排好序的数列 \(a_1,a_2,a_3,...,a_n\) 当一个新的数 \(x\) 加入这个数列的时候,最大值只能被更大的 \(x\) 更新,所以最大值不会减小,而只会增大或不变。

同理,\(a_k\)(第 \(k\) 大的数)只能被大于 \(a_k\) 的数更新,\(a_{n-k+1}\)(第 \(k\) 小的数)只能被小于 \(a_{n-k+1}\) 的数更新。所以 \(a_k\) 不会减小,\(a_{n-k+1}\) 不会增大。故 \(a_k-a_{n-k+1}\) 不会减小,即 \((a_k-a_{n-k+1})^2\) 不会减小。

因此,当一个新的数加入数列的时候,“校验值”不会减小,即“校验值”具有非严格单调性。

\(80\ \rm{Pts}\) 暴力

在“校验值”不超过 \(K\) 的情况下,不断扩大数列长度,当无法再扩大的时候,开启下一个数列。

具体地说,设当前区间为 \([l,r]\),将 \(r\) 不断右移,如果此时“校验值”大于 \(K\),则令 l=r,ans++,即开启新区间。

在每一次 \(r\) 增加的时候,将 \(a[l,r]\) 提出并排序,双指针扫描前后端并累加差的平方,即可得到校验值。

计算校验值的时间复杂度为 \(O(N \log N)\),一共需计算 \(N\) 次,所以暴力的时间复杂度为 \(O(N^2 \log N)\)

时间复杂度上可以通过 \(40\%\) 的数据,另有 \(40\%\) 的数据有特殊性质可以大量减少判断次数和判断时间,所以一共可得 \(80\ \rm{Pts}\) 暴力碾标算

参考代码:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

inline int read()
{
	int x=0;bool w=true;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')w=false;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+(ch^'0');ch=getchar();}
	return w?x:-x;
}

const int N=5e5+5,M=5e5+5;
int T,n,m,k,p[N],tmp[N];

inline bool check(const int L,const int R)
{
	for(int i=L;i<=R;i++)
		tmp[i]=p[i];
	sort(tmp+L,tmp+R+1);
	int sum=0,l=L,r=R,cnt=0;
	while(l<r && sum<=k && cnt<m)
	{
		sum+=(tmp[r]-tmp[l])*(tmp[r]-tmp[l]);
		l++,r--, cnt++;
	}
	return sum<=k;
}

int main()
{
	T=read();
	while(T--)
	{
		memset(p,0,sizeof(p));
		n=read(),m=read(),k=read();
		for(int i=1;i<=n;i++)
			p[i]=read();
		int l=1,ans=0;
		for(int r=1;r<=n;r++)
			if(!check(l,r)) l=r,ans++;
		printf("%d\n",ans+1);
	}
	return 0;
}

\(90\ \rm{Pts}\) 倍增

因为“校验值”具有单调性,所以我们可以倍增 \(r\),每次检验区间 \([l,r+2^{bin}-1]\) 是否合法,若合法则令 \(r=r+2^{bin}-1\) 并将 \(bin+1\),否则 \(bin-1\)(避免多余检测过大区间而增加程序运行时间)。

时间复杂度 \(O(N \log^2 N)\) (试图卡常卡成满分的人就是我)

参考代码:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

inline int read()
{
	int x=0;bool w=true;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')w=false;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+(ch^'0');ch=getchar();}
	return w?x:-x;
}

const int N=5e5+5;
int T,n,m;
long long k;
int p[N],tmp[N],lg2[N];

inline bool check(const int L,const int R)
{
	if(R>n) return false;
	for(int i=L;i<=R;i++)
		tmp[i]=p[i];
	long long sum=0;
	sort(tmp+L,tmp+R+1);
	int l=L,r=R,cnt=0;
	while(l<r && sum<=k && ++cnt<=m)
	{
		sum+=(long long)(tmp[r]-tmp[l])*(tmp[r]-tmp[l]);
		l++,r--;
	}
	return sum<=k;
}

int main()
{
	for(int i=2;i<=5e5;i++)
		lg2[i]=lg2[i>>1]+1;
	T=read();
	while(T--)
	{
		n=read(),m=read(); scanf("%lld",&k);
		for(int i=1;i<=n;i++)
			p[i]=read();
		int ans=0;
		int l=1,r=l,bin=1;
		while(r<=n)
		{
			if(!bin)
			{
				ans++;
				l=r+1,r=l;
			}
			if(check(l,r+(1<<bin)-1))
			{
				r=r+(1<<bin)-1;
				bin++;
			}
			else bin--;
		}
		printf("%d\n",ans);
	}
	return 0;
}

\(100\ \rm{Pts}\) 倍增+归并排序

check 的时候,尤其是 bin++ 的阶段,其所排序的数列前半段已经是有序的,无需重新将整个数列排序一遍。当数列一半有序的时候,可以考虑采用类似归并排序的合并方式

利用 C++ STL 自带的 merge 函数可以方便地合并两个有序数列为一个有序数列,无需手动再写一个 merge 函数(为什么这么多人都喜欢手写 merge,是因为不知道有这个函数吗,各位 Dalao 可以解释一下吗?)

用法:merge(原数列 A 开头, 原数列 A 结尾, 原数列 B 开头, 原数列 B 结尾, 目标数列 C 开头),注意“开头”“结尾”是指针,且不包括结尾元素(左闭右开),类似 sort 的参数传入。

另外一半的排序直接 sort 就好了,不会超时。

时间复杂度 \(O(N \log N)\)

参考代码:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

inline int read()
{
	int x=0;bool w=true;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')w=false;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+(ch^'0');ch=getchar();}
	return w?x:-x;
}

const int N=5e5+5;
int T,n,m;
long long k;
int p[N],st[N],mg[N],lg2[N];

inline bool check(const int L,const int MID,const int R)
{
	if(R>n) return false;
	for(register int i=MID;i<=R;i++)
		st[i]=p[i];
	sort(st+MID,st+R+1);
	merge(st+L,st+MID, st+MID,st+R+1, mg+L);
	long long sum=0;
	int l=L,r=R,cnt=0;
	while(l<r && sum<=k && ++cnt<=m)
	{
		sum+=(long long)(mg[r]-mg[l])*(mg[r]-mg[l]);
		l++,r--;
	}
	if(sum<=k)
	{
		for(int i=L;i<=R;i++)
		st[i]=mg[i];
	}
	return sum<=k;
}

int main()
{
	for(register int i=2;i<=5e5;i++)
		lg2[i]=lg2[i>>1]+1;
	T=read();
	while(T--)
	{
		n=read(),m=read(); scanf("%lld",&k);
		for(register int i=1;i<=n;i++)
			p[i]=read();
		int ans=0;
		int l=1,r=l,bin=1;
		st[l]=p[l];
		while(r<=n)
		{
			if(!bin)
			{
				ans++;
				l=r+1,r=l;
			}
			if(check(l,r+1,r+(1<<bin)-1))
			{
				r=r+(1<<bin)-1;
				bin++;
			}
			else bin--;
		}
		printf("%d\n",ans);
	}
	return 0;
}

注意 \(k\)\(sum\) 要开 long long


\(2025.8.12\) 更新:修正一处笔误和错别字。


2024-08-05 21:06 撰写于洛谷,2025-08-29 20:44 迁移至博客园。

posted @ 2025-08-29 20:44  Jerrycyx  阅读(20)  评论(0)    收藏  举报