【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;
}