题干:
给定一个整数 M,对于任意一个整数集合 S,定义“校验值”如下:
从集合 S 中取出 M 对数(即 2∗M个数,不能重复使用集合中的数,如果 S 中的整数不够 M 对,则取到不能取为止),使得“每对数的差的平方”之和最大,这个最大值就称为集合S的“校验值”。
现在给定一个长度为 N 的数列 A 以及一个整数 T。
我们要把 A 分成若干段,使得每一段的“校验值”都不超过 T。
求最少需要分成几段。
1≤K≤12
1≤N,M≤500000
0≤T≤
1
0
18
10^{18}
1018
0≤Ai≤
2
20
2^{20}
220
思路
参考自《算法竞赛进阶指南》和这篇博客
根据贪心原则,每次选最大的数减去最小的数得到的每对数的差的平方”之和最大,则校验值可以由sum+=(mx-mi)*(mx-mi)得到(mx和mi每次更新)
然后为了快速得到mi和mx,我们可以提前将数组排序,考虑到我们还需要使用原数组,所以我们需要一个来记录。
首先需要得到一个最长的满足条件的队列,作为一次划分。这样我们需要枚举长度d,又因为1≤N≤500000,所以可以考虑到二分范围,但当T比较小的时候,需要的d较小而二分到小和大值都比较慢,所以为了解决这个问题可以使用倍增的方法,即d从1开始,如果合适 d ∗ * ∗=2,否则d/=2.
这样我们得到了需要排序的区间,但是直接快排时间还是不大够,考虑到归并排序是每次分成两部分,则我们可以得到两个已经排好的序列,这样我们在下一次倍增的时候可以减少重复运算。
最后注意T的范围爆了int,所以在存值的时候要用long long
#include <cstdio>
#include <cstring>
#include <cmath>
#include <queue>
#include <map>
#include <iostream>
#include <algorithm>
#define ll long long
using namespace std;
int k,n,m;
ll a[500010],b[500010],t;
bool check(vector<int> vector);
bool combine(int l, int r, int nr){
sort(b+1+r,b+1+nr);
vector<int> v;
int q=l,p=r+1;
while(q<=r&&p<=nr){
if(b[q]<b[p]) v.push_back(b[q++]);
else v.push_back(b[p++]);
}
while(q<=r)
v.push_back(b[q++]);
while(p<=nr)
v.push_back(b[p++]);
if(check(v)){
for(int i=0,j=l;i<v.size();i++,j++){
a[j]=b[j]=v[i];
}
return true;
} else{
for(int i=r+1;i<=nr;i++){
b[i]=a[i];
}
return false;
}
}
bool check(vector<int> v) {
if(v.size()<=1) return true;
ll s=0;
int l=0,r=v.size()-1,sum=m;
while(l<r&&sum){
ll c=v[r]-v[l];
s+=c*c;
if(s>t) return false;
l++;r--;sum--;
}
return true;
}
int main()
{
scanf("%d",&k);
while(k--){
scanf("%d%d%lld",&n,&m,&t);
for(int i=0;i<n;i++){
scanf("%lld",&a[i]);
b[i]=a[i];
}
int l=0,r=0,ans=0;
while(l<n){
int nr=0,d=1;
while(d)
{
nr=r+d;
if(nr<n&&combine(l,r,nr)) {
d=d*2;
r=nr;
}
else d=d/2;
}
ans++;
l=r=min(r+1,n);
}
printf("%d\n",ans);
}
return 0;
}