倍增
一、倍增
倍增,顧名思義,成倍增長。一般我們在進行遞推時,如果狀態空間很大,通常的線性遞推無法滿足時間與空間復雜度的要求,那么我們可以通過成倍增長的方式,只遞推狀態空間中在2的整數次冪位置上的值作為代表。
當需要其他位置上的值時,我們通過“任意整數可以表示成若干個2的次冪項的和”這一性質,使用之前求出的代表值拼成所需的值。所以使用倍增算法也要求我們遞推的問題的狀態空間關於2的次冪具有可划分性。本文中,我們研究序列上的倍增算法。
- 試想這樣一個問題
給定一個長度為 \(N\) 的數列 \(A\),然后進行若干次訪問,每次給定一個整數 \(T\),求出最大的 \(k\) 滿足 \(\sum_{i=1}^{k}A[i]\leq T\)。
你的算法必須是在線的(必須即時回答每一個詢問),假設\(0\leq T\leq \sum_{i=1}^{N}A[i]\)。
最朴素的做法肯定是暴力枚舉 \(k\) ,其次是處理前綴和數組后二分答案,但這些方法的時間復雜度都不是很優。
我們可以設計這樣的倍增算法:
- 令 \(p=1,k=0,sum=0\)。
- 比較“ \(A\) 數組 \(k\) 之后的 \(p\) 個數的和”與 \(T\) 的關系。
if sum + S[k + p] - S[k] <= T
then sum += S[k + p] - S[k], k += p, p *= 2
else then p /= 2
- 重復上一步,知道 \(p\) 的值變為 \(0\),此時 \(k\) 就是答案。
- 【例題】Genius ACM
給定一個整數 \(M\),對於任意一個整數集合 \(S\),定義校驗值如下:
從集合 \(S\) 中取出 \(M\) 對數(即 \(2 * M\) 個數,不能重復使用集合中的數,如果 \(S\) 中的整數不夠 \(M\) 對,則取到不能取為止),使得“每對數的差的平方”之和最大,這個數就稱為集合 \(S\) 的校驗值。
現在給定一個長度為 \(N\) 的數列 \(A\) 以及一個整數 \(T\)。我們要把 \(A\) 分成若干段,使得每一段的“校驗值”都不超過 \(T\)。求最少需要分成幾段。
解題思路:我們從頭開始對 \(A\) 進行分段,讓每一段盡量長,到達結尾時分成的段數就是答案。
因此需要解決的問題為:當確定一個左端點 \(L\) 后,右端點 \(R\) 在滿足 \(A[L]\) ~ \(A[R]\) 的“校驗值”不超過 \(T\) 的前提下,最大能取到多少。
問題解決:
1.怎么求長度為 \(N\) 的一段的校驗值?排序后取左右端點,再求校驗值即可。
2.區間的校驗值求法,我們采用與上面例子類似的倍增方法。
算法思路:
- 初始化 \(p=1,R=L\)。
- 求出 \([L,R+p]\) 這一段區間的校驗值,若校驗值 \(\leq T\),則 \(R+=p,p *= 2\),否則 \(p/=2\)。
- 重復上一步,直到 \(p\) 的數值變為 \(0\),此時 \(R\) 即為所求。算法時間復雜度為 \(O(N\log^2 N)\)
//AcWing109, AC代碼
#include<cstdio>
#include<algorithm>
using namespace std;
const int MAXN = 500005;
long long a[MAXN], b[MAXN], t;
int n, m, k;
long long sq(long long x){
return x * x;
}
long long check(int L, int R){
int cnt = 0;
for(int i = L; i < R; i++) b[cnt++] = a[i];
sort(b, b + cnt);
long long sum = 0;
for(int i = 0; i < m && i < cnt; i ++, cnt --) sum += sq(b[i] - b[cnt - 1]);
return sum;
}
int main(){
scanf("%d", &k);
while(k--){
scanf("%d%d%lld", &n, &m, &t);
for(int i = 0; i < n; i++) scanf("%lld", &a[i]);
int ans = 0;
int L = 0, R = 0;
while(R < n){
int p = 1;
while(p){
if(R + p <= n && check(L, R + p) <= t)
R += p, p *= 2;
else p /= 2;
}
L = R;
ans++;
}
printf("%d\n", ans);
}
return 0;
}
二、ST算法
在 \(RMQ\) 問題(區間最值問題)中,著名的 \(ST\) 算法就是倍增的產物。\(RMQ\) 問題描述如下:
給定一個長度為 \(N\) 的數列 \(A\),並有 \(M\) 次詢問,每次詢問給定下標 \(l,r\),在線回答“數列 \(A\) 中下標在 \(l\) ~ \(r\) 之間的數的最大值”。
對於這種問題,\(ST\) 算法能做到在 \(O(N \log^2 N)\) 時間的預處理后,以 \(O(1)\) 的時間復雜度回答每個詢問。
問題解決 && 算法思路:
- 初始化
一個序列的子區間個數顯然有 \(n^2\) 個,根據倍增思想,我們在這個狀態空間中選擇一些2的整數次冪的位置作為代表值。
設 \(F[i,j]\) 表示數列 \(A\) 中下標在子區間 \([i,i + 2^j - 1]\) 里的數的最大值,也就是從 \(i\) 開始的 \(2^j\) 個數中的最大值。遞推邊界顯然為 \(F[i,0]=A[i]\),即數列 \(A\) 在子區間 \([i,i]\) 中的最大值。
遞推時,將子區間成倍增長,有公式
即長度為 \(2^j\) 的區間的最大值時左右兩半長度為 \(2^{j-1}\) 的子區間的最大值中的最大值。
void ST_prework(){
for(int i = 1; i <= n; i++) f[i][0] = a[i];
int t = log(n) / log(2) + 1;
for(int j = 1; j < t; j++)
for(int i = 1; i <= n - (1 << j) + 1; i++)
f[i][j] = max(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);
}
- 詢問
當詢問任意區間 \([l,r]\) 的最值時,先計算出一個 \(k\),使得 \(2^k\leq r-l+1\leq 2^{k+1}\),也就是2的 \(k\) 次冪小於區間長度的 \(k\) 的最大值。那么從 \(l\) 開始的 \(2^k\) 個數與以 \(r\) 結尾的 \(2^k\) 個數一定覆蓋了區間 \([l,r]\),這兩段的最大值分別為 \(F[l][k], F[r - 2^k+1][k]\),二者的最大值就是整個區間的最大值。
int ST_query(int l, int r){
int k = log(r - l + 1) / log(2);
return max(f[l][k], f[r - (1<<k) + 1][k]);
}
以上程序使用 cmath 庫中的 \(\log\) 函數,如果想要進一步追求效率,可以先用 \(O(N)\) 的時間預處理 \(1\) ~ \(N\) 每個數字所對應的 \(\log\) 值。
void init(){
for(int i = 1; i <= n; i++) Log[i] = log(i);
}
關於求解 \(LCA\) 的應用將在后面補充。