0x06
倍增
倍增,字面意思就是“成倍增长”。我们在递推时,如果状态空间很大,通常的线性递推无法满足时间与空间复杂度的要求,那么我们可以使用成倍增长的方式,只递推状态空间在2的整数次幂上的值作为代表。当需要其他位置上的值时,我们通过“任意整数可以表示成若干个2的次幂项的和”这一性质,使用之前求出的代表值拼成所需的值。所以使用倍增算法也要求我们递推的问题的状态空间关于2的次幂具有可划分性。
“倍增”与“二进制划分”两个思想相互结合,降低了很多问题的时间与空间复杂度。我们之前学习的快速幂其实就是“倍增”与“二进制划分”思想的一种体现。
试想这样一个问题:
给定一个长度为N的数组A,然后进行若干次询问,每次给定一个整数T,求出最大的k,满足 ∑ i = 1 k A [ i ] ≤ T \sum_{i=1}^{k}A[i]\leq T ∑i=1kA[i]≤T。你的算法必须是在线的(必须及时回答每一个询问),假设 0 ≤ T ≤ ∑ i = 1 N A [ i ] 0\leq T \leq \sum_{i=1}^{N}A[i] 0≤T≤∑i=1NA[i]。
最朴素的做法是从前往后枚举k,每次询问花费的时间与答案的大小有关,最坏情况下为O(n)
。
我们可以花费O(n)
的时间预处理数组A,得到前缀和序列S,就可以二分k的位置,每次询问花费的时间为O(logn)
。这个算法在平均情况下表现得非常好,但它的缺点是当每次给定的T都很小,造成答案的k值也很小,那么该算法可能还不如从前往后枚举更优。
我们可以设计一种倍增算法:
1.令p=1,k=0,sum=0;
2.比较“A数组k之后的p个数之和与”与T的关系,也就是说当 s u m + S [ k + p ] − S [ k ] ≤ T sum+S[k+p]-S[k]\leq T sum+S[k+p]−S[k]≤T ,则令 s u m + = S [ k + p ] − S [ k ] , k + = p , p ∗ = 2 sum+=S[k+p]-S[k],k+=p,p*=2 sum+=S[k+p]−S[k],k+=p,p∗=2,即累加上这p个数之和,然后把p的跨度增加一倍。如果 s u m + S [ k + p ] − S [ k ] > T sum+S[k+p]-S[k]>T sum+S[k+p]−S[k]>T,则令 p / = 2 p/=2 p/=2。
3.重复上一步直到p=0,此时k就是答案。
这个算法始终在答案范围内进行实施“倍增”和“二进制划分”思想,通过若干长度为2的次幂的区间拼成最后的k,时间复杂度为答案的对数,能够对应T的各种情况大小。
很多二分答案的想法都可以转化成倍增。
1. ST算法
在
R
M
Q
RMQ
RMQ问题中(区间最值问题),著名的ST算法就是倍增的产物。给定一个长度为N的序列A,ST算法能在O(NlogN)
时间的预处理后,以O(1)
的时间时间复杂度在线回答“数列A中下标在
l
∼
r
l\sim r
l∼r之间的最大值是多少”这样的区间最值问题。
一个序列的子区间有 N ∗ ( N + 1 ) 2 \frac{N*(N+1)}{2} 2N∗(N+1)个,根据倍增思想,我们首先在这个规模为 O ( N 2 ) O(N^2) O(N2)的状态空间中选择一些2的整数次幂的位置作为代表值。
设 F [ i ] [ j ] F[i][j] F[i][j]表示数列A中下标在子区间 [ i , i + 2 j − 1 ] [i,i+2^j-1] [i,i+2j−1]里的最大值,也就是从 i i i开始的 2 j 2^j 2j个数中的最大值。递推边界显然是 F [ i ] [ 0 ] = A [ i ] F[i][0]=A[i] F[i][0]=A[i]。
在递推时,我们把区间的长度成倍增长,有公式 F [ i ] [ j ] = m a x ( F [ i ] [ j − 1 ] , F [ i + 2 j − 1 ] [ j − 1 ] ) F[i][j]=max(F[i][j-1],F[i+2^{j-1}][j-1]) F[i][j]=max(F[i][j−1],F[i+2j−1][j−1]),即长度为 2 j 2^j 2j的子区间的最大值是左右两个长度为 2 j − 1 2^{j-1} 2j−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 ] [l,r] [l,r]的最值时,我们先计算出一个k,满足 2 k ≤ r − l + 1 < 2 k + 1 2^k\leq r-l+1< 2^{k+1} 2k≤r−l+1<2k+1 ,也就是使2的k次幂小于等于区间长度前提下的最大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
(
1
)
O(1)
O(1),应该
O
(
N
)
O(N)
O(N)预处理出
1
∼
N
1\sim N
1∼N这
N
N
N种区间长度各自对应的
k
k
k值,在询问时直接使用。