# Acwing 109. Genius ACM (倍增 + 归并排序)

题意

给出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;
}
原文地址:https://www.cnblogs.com/sstealer/p/13297698.html