《算法竞赛进阶指南》0x06倍增 Acwing GeniusACM

题目链接:https://www.acwing.com/problem/content/description/111/

首先定义了集合S的校验值,取出m对数,使得每对平方之后求和最大,这个值成为集合S的校验值。现在给定一个数列,求满足每段的校验值小于T的前提下最小能把数列分成连续的几段?

利用倍增的思想对右端点进行倍增,并且结合归并排序去除冗余的排序。具体解释在代码中阐明。二进制就是这么神奇而高效。

代码如下:

#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;
#define maxn 500010
typedef long long ll;
int n,m,ed;
ll k;//注意k是1e18范围的 
ll a[maxn],b[maxn],c[maxn];//a:无序,原数组,b:分两段有序,c:归并之后的合并数组
void gb(int l,int mid,int r){//[l,mid]与[mid+1,r] 归并,
    int i=l,j=mid+1;//双指针扫描
    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++];
    } 
} 
ll calc(int l,int r){//计算校验值 
    if(r>n)r=n;
    int t=min((r-l+1)/2,m);//将[l,r]区间分成的最大的对数
    ll ans=0;
    for(int i=ed+1;i<=r;i++) b[i]=a[i];//将a[i]中上次mid+1的位置到r的序列放入b中并排序
     sort(b+ed+1,b+r+1);//[ed+1,r]部分排序
     gb(l,ed,r);
     for(int i=0;i<t;i++){
         ans+=(c[r-i]-c[l+i])*(c[r-i]-c[l+i]);
     } 
     return ans;
}
void Genius_ACM(){
    int ans=0;//分的段数
    int l=1,r=1;
    b[1]=a[1];
    ed=1;//ed表示的是上次倍增的结束位置
    while(l<=n){
        int p=1;//每一段都是从2^0开始倍增的
        while(p){
            ll num=calc(l,r+p);
            if(num<=k){
                ed=r=min(r+p,n);
                for(int i=l;i<=r;i++)b[i]=c[i];//将归并的有序序列放入b中 
                 if(r==n)break;
                 p<<=1;
            }else p>>=1;//倍增失败,缩小倍增跨度
        } 
        ans++;
        l=r+1;
    } 
    cout<<ans<<endl;
}
int main(){
    int t;
    cin>>t;
    while(t--){
        cin>>n>>m>>k;
        for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
        Genius_ACM();
    }
    return 0;
}
原文地址:https://www.cnblogs.com/randy-lo/p/13137638.html