天才ACM(倍增)
題目
題解
參考題解:
https://www.acwing.com/solution/content/15458/
很好。
首先考慮我們用貪心證明兩個東西:
- 如果第\(i\)個可以歸到前面的部分就歸到前面的部分,不要放到後面的部分,反正放到後面也只會讓校驗值增大,還不如不放。
- 對於一個數列而言,如何求校驗值?答案是最大的減最小的的平方加上次大的...。
至於證明,首先,如果對於\((x-y)^2(x>y)\),而數列中有 沒有用的比\(x\)更大或比\(y\)更小的數字的話,那麼我們肯定要用啊。
第二,對於\(a<b<c<d\)而言,最大的方案肯定是:\((a-d)^2+(b-c)^2\)
通過這兩個結論就可以證明了。
方法1
因為長度越長校驗值越大,根據這個可以二分,也就是二分+check,但是如果每個塊都很小的話時間複雜度就很容易被卡成:\(O(Kn^2log^2n)\),。
方法2
這個時候就要用到倍增了,倍增演算法完美解決了二分處理小資料時無力的問題,倍增的流程是:
- 確定一個\(x=1,R=1\),然後如果\([1,R+x]\)可以的話,那麼\(R+=x,x<<=1\),並重復1操作,但是如果不可以的話,\(x>>=1\)
- 如果[1,R+x]可以的話,\(R+=x\),不管可不可以,\(x>>=1\)並重復2操作直到\(x=0\)。
當然還有一種較慢的倍增:是如果不可以\(x>>=1\),可以\(x<<=1\),但是會慢(因為其實也是跟上面一樣的流程,但是在2步驟中一旦可以會\(x<<=1\)又要花一次\(x>>=1\)來重回正軌)。
(其實我一開始的寫法是是1操作沒有\(R+=x\),跳到2操作時再\(R=x\),但是沒必要,還會慢)
當然,上面的三種做法,單次的時間複雜度都是\(O(\log{t})\)(\(t\)為最終的\(R\)),因此倍增是一個時間複雜度很優秀的演算法,而且在處理資料塊很小時更是展現出比二分大得多的優越性。(至於為什麼,你可以把第一個步驟看成找最大\(?\)
採用快速排序的時間複雜度是\(O(nlog^2n)\)。
證明我先寫的下面的證明,哈哈:
設第\(i\)個塊長度為\(l_i\),且在倍增第一個步驟中\(x\)最大去到了\(xx\)。
注:計算校驗值的時間複雜度是塊長,而排序的複雜度大於等於塊長,因此預設忽略校驗值的時間複雜度計算。
第一個步驟是一個\(log\),我們不管他,我們要證的是第二個步驟,第一個步驟完畢後,確定的\(R\)大於等於\(\frac{l_i}{2}\),因為要check \(logl_i\)次,所以時間複雜度為:\(O(Rlogl_ilogR)=R(l_ilog^2l_i)\)。
所有塊合併起來的時間複雜度就是:\(O(nlog^2n)\)。
常數小可以AC此題。
#include<cstdio>
#include<cstring>
#include<algorithm>
#define N 510000
using namespace std;
int a[N],b[N],n,m;
long long T;
inline int mymin(int x,int y){return x<y?x:y;}
bool check(int x,int now)
{
int l=now+1,r=x+now;
int t=0;
for(int i=l;i<=r;i++)b[++t]=a[i];
sort(b+1,b+t+1);
int f=t/2;
long long ans=0;
for(int i=mymin(f,m);i>=1;i--)ans+=(long long)(b[t-i+1]-b[i])*(b[t-i+1]-b[i]);
return ans<=T;
}
int main()
{
int K;scanf("%d",&K);
while(K--)
{
scanf("%d%d%lld",&n,&m,&T);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
int now=0,ans=0;
while(now<n)
{
int cnt=1,R=1;
while(check(mymin(R+cnt,n-now),now)==1)
{
R+=cnt;
if(R>n-now){R=n-now;break;}
cnt*=2;
}
if(cnt!=n-now)
{
cnt/=2;
while(cnt)
{
if(check(mymin(R+cnt,n-now),now)==1)
{
R+=cnt;
if(R>n-now){R=n-now;break;}
}
cnt>>=1;
}
}
now+=R;
ans++;
}
printf("%d\n",ans);
}
return 0;
}
方法3
我們上面採用的是快速排序,但是如果用類似歸併排序的方法呢?
即我現在已經知道了\(X\),然後要對\(X+Y\)進行排序,那麼就對\(Y\)進行排序,然後合併。
這個優化在較快的倍增中的第一個步驟中是毫無優化的,因為:\(\frac{n}{2}*\log {\frac{n}{2}}*2+n=n\log{n}\)。
但是再第二個步驟中,就不會因為長度較小的\(Y\)而重複排序了,優化程度很大。
(下面證明用的是目前講的最快的倍增打法)
那麼我們現在就來證明一波時間複雜度:
設第\(i\)個塊長度為\(l_i\),且在倍增第一個步驟中\(x\)最大去到了\(xx\)。
注:計算校驗值的時間複雜度是塊長,而排序的複雜度大於等於塊長,因此預設忽略校驗值的時間複雜度計算。
對於第一個步驟而言:\(O(1*log(1)+3*log(2)+7*log(4)+...+(2*xx-1)*log(xx)+(4*xx-1)*log(xx*2))=O(xx\log{xx})\)(根據\(a_1\log{a_1}+a_2\log{a_2}+...+a_i\log{a_i}≤sum\log{sum}\)得出,\(sum\)為總和)
對於第二個步驟而言:
合併的話,即使假設單次合併的的時間是最終塊長的兩倍,那麼時間複雜度也是:\(O(log_{l_i}l_i)\),所以就是分析快速排序的時間複雜度:\(O(1*log(1)+2*log(2)+4*log(4)+...+\frac{xx}{2}*log(\frac{xx}{2})=O(xx\log{xx})\),因此對於一個塊而言,時間複雜度即為:\(O(l_i\log{l_i})\),所有塊時間複雜度一合併,就是\(O(nlogn)\)了。
程式碼:
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 500005;
int n, m;
int ans;
ll T;
ll w[N], t[N];
ll tmp[N];
ll sq(ll x)
{
return x * x;
}
bool check(int l, int mid, int r) // 判斷區間 [l, r) 是否合法,並將 t 中的 [l, mid) 區間和 [mid, r) 區間合併到 tmp 中
{
for (int i = mid; i < r; i ++ ) // 將 w 陣列的 [l, r) 區間複製到 t 的 [l, r) 區間中
t[i] = w[i];
sort(t + mid, t + r); // 將 t 的 [mid, r) 排序
int i = l, j = mid, k = 0; // 雙指標進行區間合併
while (i != mid && j != r) // 當 i 不到 mid 且 j 不到 r 時,執行迴圈
if (t[i] < t[j]) // 如果 t[i] 比 t[j] 小,那麼將 t[i] 放入 tmp 中
tmp[k ++ ] = t[i ++ ];
else // 否則將 t[j] 放入 tmp 中
tmp[k ++ ] = t[j ++ ];
while (i != mid) tmp[k ++ ] = t[i ++ ]; // 處理剩下的元素
while (j != r) tmp[k ++ ] = t[j ++ ];
ll sum = 0; // 計算校驗值
for (i = 0; i < m && i < k; i ++ , k -- )
sum += sq(tmp[i] - tmp[k - 1]);
return sum <= T; // 返回當前區間 [l, r) 是否合法
}
int main()
{
int K;
scanf("%d", &K);
while (K -- )
{
scanf("%d %d %lld\n", &n, &m, &T);
for (int i = 0; i < n; i ++ )
scanf("%lld", &w[i]);
ans = 0;
int len;
int start = 0, end = 0;
while (end < n)
{
len = 1;
while (len)
{
if (end + len <= n && check(start, end, end + len)) // 如果 w 的 [start, end + len) 區間合法
{
end += len, len <<= 1;
if (end >= n) break ; // 一個小剪枝,如果 end >= n,那麼直接跳出
for (int i = start; i < end; i ++ ) // 在 check 時,已經將 t 陣列的 [start, end + len) 這段區間歸併在 tmp 中了。現在只需要將 tmp 中的有序陣列複製到 t 中即可
t[i] = tmp[i - start]; // 複製的時候注意下標變換,tmp 是從 0 開始存的,t 是從 start 開始存的
}
else len >>= 1;
}
start = end;
ans ++ ;
}
printf("%d\n", ans);
}
return 0;
}
作者:墊底抽風
連結:https://www.acwing.com/solution/content/15458/
來源:AcWing
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。