题意
给出n个数Ai,问最少将这n个数分成几段,使每一段的校验值小于k。
一段数的校验值定义为:
从集合 S 中取出 M对数(即 2∗M 个数,不能重复使用集合中的数,如果 S 中的整数不够 M 对,则取到不能取为止),使得“每对数的差的平方”之和最大,这个最大值就称为集合 S 的“校验值”。
(1<= N , M <=500000,0<=K<=1e18,0<=A_i<=2^{20})
思路
贪心很容易得到校验值,就是选最大和最小的差的平方+次大和次小的差平方+…...+。
要想分段数最小,每一段就要尽可能多的数。对于一个左端点,使右端点尽可能大。
一种很朴素的思路是一点一点扩展右端点,每次扩展排序算校验值,但是这样扩展太慢,并且涉及大量的快排,显然会超时。
优化:
扩展可以考虑使用倍增思想,以二进制的方式扩展。
每次扩展其实只有扩展的部分需要重新排序,可以考虑只对扩展的部分使用快排,再使用归并排序合并。
代码
#include <bits/stdc++.h>
using namespace std;
#define doub(x) (x)*(x)
typedef long long LL;
const int maxn=5e5+5;
int t,n,m,ans;
LL k;
int p[maxn],a[maxn];
LL b[maxn];
inline void merge(int l,int mid,int r){
int i=l,j=mid,k=l;
while (i<mid && j<=r){
if(a[i]<a[j])b[k++]=a[i++];
else b[k++]=a[j++];
}
while (i<mid)b[k++]=a[i++];
while (j<=r)b[k++]=a[j++];
}
inline bool check(int l,int mid,int r){
for(int i=mid;i<=r;++i)a[i]=p[i];
sort(a+mid,a+r+1);
merge(l,mid,r);
LL sum=0;
for(int i=0;i<m && r-i>l+i ;++i)
sum+=doub(b[r-i]-b[l+i]);
if(sum>k)return false;
for(int i=l;i<=r;++i)a[i]=b[i];
return true;
}
inline void sol(){
ans=0;
int l=0,r=0,len=1;
a[l]=p[l];
while (r<n){
if(!len){
++ans;
l=(++r);
a[l]=p[l];
len=1;
}
else if(r+len<n && check(l,r+1,r+len)){
r+=len;
if(r==n-1)break;
len<<=1;//倍增思想,成倍增长
}
else len>>=1;//倍增思想,无法成倍增长时,尝试缩小一倍增长
}
if(r==n-1)++ans;
}
int main(){
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>t;
while (t--){
cin>>n>>m>>k;
for(int i=0;i<n;++i)cin>>p[i];
sol();
cout<<ans<<endl;
}
return 0;
}