1. 程式人生 > 實用技巧 >天才ACM(倍增)

天才ACM(倍增)

題目

題目

題解

參考題解:
https://www.acwing.com/solution/content/15458/
很好。

首先考慮我們用貪心證明兩個東西:

  1. 如果第\(i\)個可以歸到前面的部分就歸到前面的部分,不要放到後面的部分,反正放到後面也只會讓校驗值增大,還不如不放。
  2. 對於一個數列而言,如何求校驗值?答案是最大的減最小的的平方加上次大的...。
    至於證明,首先,如果對於\((x-y)^2(x>y)\),而數列中有 沒有用的比\(x\)更大或比\(y\)更小的數字的話,那麼我們肯定要用啊。
    第二,對於\(a<b<c<d\)而言,最大的方案肯定是:\((a-d)^2+(b-c)^2\)
    ,要證明的話全部化成平方和就變成了問兩兩數字相乘最小的問題了:\(a^2+b^2+c^2+d^2-2ad-bc\),這個大家肯定都會證明的吧,不會證明看這裡:

    通過這兩個結論就可以證明了。

方法1

因為長度越長校驗值越大,根據這個可以二分,也就是二分+check,但是如果每個塊都很小的話時間複雜度就很容易被卡成:\(O(Kn^2log^2n)\),。

方法2

這個時候就要用到倍增了,倍增演算法完美解決了二分處理小資料時無力的問題,倍增的流程是:

  1. 確定一個\(x=1,R=1\),然後如果\([1,R+x]\)可以的話,那麼\(R+=x,x<<=1\),並重復1操作,但是如果不可以的話,\(x>>=1\)
    並跳到2操作。
  2. 如果[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\)),因此倍增是一個時間複雜度很優秀的演算法,而且在處理資料塊很小時更是展現出比二分大得多的優越性。(至於為什麼,你可以把第一個步驟看成找最大\(?\)

使得\(2^{?}-1≤t\),至於第二個步驟,則是又把\(?\)遍歷一遍,所以是\(O(\log{t})\)

採用快速排序的時間複雜度是\(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
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。