【ContestHunter0601】Genius ACM-贪心+倍增+归并排序
测试地址:Genius ACM
做法: 本题需要用到贪心+倍增+归并排序。
某机房大佬给的我他书上的一道神题…据说还是“基础算法”章节的例题…看来我NOIP退役已经是可以预见的了…
首先可以大胆猜想(并小心证明)的是,计算校验值时所选的对数,一定是最大的与最小的配对,次大的与次小的配对…以此类推。那么很明显的,一个区间如果被另一个区间包含,那么被包含的区间的校验值一定更小,这就是区间包含单调性,因此要求至少要分多少段,只要从头开始暴力向右扩展,扩展不了了就分段即可。
那么现在问题的关键是,如何在这个算法的过程中快速地算出校验值?我们发现这种信息用数据结构很难维护,于是我们先思考一个暴力:右端点每扩展一步,就重新对当前区间内的元素排一次序。使用插入排序的话,上述算法最坏情况下是的。于是我们思考,产生重复性的关键问题在哪里呢?显然,如果每次仅插入一个元素,排序的次数很大,重复性也会很高。因此,我们尝试使用倍增的思路,每次加入个元素,来降低排序的次数。
一个很明显的思路是,像一般的倍增一样,从大到小枚举,然后check一下区间合不合法,如果合法就给加上。而check时,我们能想到的最好的方法就是,对排序,然后把这个区间和我们已经求出的进行归并。但这样的问题是,check的时间复杂度是的,整个算法中要check的次数也较多,姑且算的级别,那也是会爆炸的。因此我们需要使用一种更改过的倍增算法,如下:
1.一开始。
2.判断合不合法,合法则更新为,然后令,否则令,重复。
3.当上述步骤执行到时,算法结束。
我们来看一下这个算法比传统倍增好在哪里。首先,可以肯定的是任何情况下,check的次数都为级别(对求出一次分段点而言),其中为最终分出的段长。然后,这个算法中的先从小到大,然后再从大到小,这就避免了check复杂度中的那个过大。显然不会超过。这样一来我们再来分析这个算法的时间复杂度。对求出一次分段点而言,令分出的这一段长度为,那么因为不会超过,所以扩展时check的时间复杂度中,这样的部分的总和是的级别,常数会稍大一些。而因为check最多进行次,那么check复杂度后面那个部分的总和也是的级别。那么对于整个序列,check的时间复杂度总和就是,这也就是算法的总时间复杂度了。于是这个问题就被完美的解决了。
以下是本人代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int T,n,m,nowsiz;
ll k,a[500010],now[500010],t[500010],tmp[500010];
bool check(int L,int R)
{
if (R>n) return 0;
}
bool check(int L,int R,int p)
{
if (R+p>n) return 0;
for(int i=1;i<=p;i++)
t[i]=a[R+i];
sort(t+1,t+p+1);
int id1=0,id2=0;
for(int i=1;i<=R+p-L+1;i++)
{
if (id1>=nowsiz) tmp[i]=t[++id2];
else if (id2>=p) tmp[i]=now[++id1];
else if (now[id1+1]<t[id2+1]) tmp[i]=now[++id1];
else tmp[i]=t[++id2];
}
ll ans=0;
for(int i=1,j=R+p-L+1;i<j&&i<=m;i++,j--)
ans+=(tmp[j]-tmp[i])*(tmp[j]-tmp[i]);
return ans<=k;
}
int solve(int L)
{
int R=L,p=1;
now[1]=a[L],nowsiz=1;
while(p)
{
if (check(L,R,p))
{
R+=p;
nowsiz=R-L+1;
for(int i=1;i<=nowsiz;i++)
now[i]=tmp[i];
p<<=1;
}
else p>>=1;
}
return R;
}
int main()
{
scanf("%d",&T);
while(T--)
{
scanf("%d%d%lld",&n,&m,&k);
for(int i=1;i<=n;i++)
scanf("%lld",&a[i]);
int st=1,ans=0;
while(st<=n)
{
st=solve(st)+1;
ans++;
}
printf("%d\n",ans);
}
return 0;
}