0x06 倍增

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] 0Ti=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 lr之间的最大值是多少”这样的区间最值问题。

一个序列的子区间有 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+2j1]里的最大值,也就是从 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][j1],F[i+2j1][j1]),即长度为 2 j 2^j 2j的子区间的最大值是左右两个长度为 2 j − 1 2^{j-1} 2j1的子区间的最大值中较大的一个。

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} 2krl+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 1N N N N种区间长度各自对应的 k k k值,在询问时直接使用。

你可能感兴趣的:(#,0x00,基本算法,算法,数据结构,c++)