题目
给定一个整数 m,对于任意一个整数集合 S,定义“校验值”如下:从集合 S 中取出 m 对数(即 2*M 个数,不能重复使用集合中的数,如果 S 中的整 数不够 m 对,则取到不能取为止),使得“每对数的差的平方”之和最大,这个最大值 就称为集合 S 的“校验值”。
现在给定一个长度为 n 的数列 A 以及一个整数 k。我们要把 A 分成若干段,使得 每一段的“校验值”都不超过 k。求最少需要分成几段。
题解
倍增+排序+暴力
题意中“每对数的差的平方”之和最大是首要解决任务,根据数学猜想,很容易想到把最大和最小组合,把次大和次小组合,以此类推。这里就涉及到了顺序问题。朴素做法很容易想到,从1往n推,一旦超过k,就切换到下一个区间。让每个区间都尽量大,最终的段数一定最小。
这样的枚举实在太慢了,要优化有两条路可以走,一是二分,二是倍增,我选择了倍增做法。
其实倍增和二分待解决的问题是一样的,都是要做一个判断l~r的校验值是否在k之内。
第一步,最麻烦的,使区间内的元素有序。最简单的方法就是每加一个数进来就排一次序,可是TLE了。优化一下,把新增区间用归并排序并到原区间中。但是不要着急赋给它,因为有可能会失败。要等到符合题意后,再赋值。
第二步,求出该区间最大的“每对数的差的平方”之和。
第三步,判断要增大r,还是坎半p。
代码
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
const int maxn=5e5+10;
inline ll read()
{
ll re=0;char ch=getchar();
while(ch<'0'||ch>'9') ch=getchar();
while(ch>='0'&&ch<='9') re=re*10+(ch^48),ch=getchar();
return re;
}
int n,m;ll k;
int a[maxn],b[maxn],c[maxn];
//a存贮原始数据 b存当前排好序的序列 c存最新排好序的序列(注意:只在特定范围内有序)
void merge(int l,int r,int mid)//合并b l~mid-1和mid~r
{
int i=l,j=mid;
for(int k=l;k<=r;k++)
if( j>r || (i<mid && b[i]<b[j]) ) c[k]=b[i++];
else c[k]=b[j++];
// for(int k=l;k<=r;k++) b[k]=c[k];//推迟
}
ll ans,p,l,r;
void mysort(int al,int ar)
{
for(int i=r+1;i<=ar;i++) b[i]=a[i];
sort(b+r+1,b+ar+1);
merge(al,ar,r+1);
}
ll calc(int al,int ar)
{
mysort(al,ar);
ll re=0,cnt=0;
for(int i=al,j=ar;i<j;i++,j--)
{
re+=(ll)(c[i]-c[j])*(c[i]-c[j]);//用最新的排好序的c来求校验值
cnt++;if(cnt==m) break;
}
return re;
}
int main()
{
int T;scanf("%d",&T);
while(T--)
{
n=read();m=read();k=read();
for(int i=1;i<=n;i++) a[i]=read();
ans=0,l=1;
while(l<=n)
{
ans++;
r=l;p=1;
b[l]=a[l];//p=0先放入a[l]
while(p!=0)
{
if( r+p<=n && calc(l,r+p)<=k )
{
r+=p;p<<=1;//倍增
for(int i=l;i<=r;i++) b[i]=c[i];//正式赋值
}
else p>>=1;//倍减
}
l=r+1;
}
printf("%lld\n",ans);
}
return 0;
}