引言
关于倍增算法,我们先引入一个情景:若在一场直线比赛中,不考虑现实因素,一步一跑,跳着跑,跳着跑且速度叠加,谁跑的更快?很明显,跳着跑且速度叠加跑的更快,因为这种方法的速度是以指数级别 增加的。
其实上述所讲的"叠加速度跑法",近似于倍增算法的基本逻辑。
倍增算法
倍增算法的核心就是成倍的跳跃(成倍增长) ,这种指数级别跳跃往往可以将时间复杂度降低为 O ( l o g n ) O(logn) O(logn)或 O ( n l o g n ) O(nlogn) O(nlogn)。正常情况下是以每次成2作为成长倍数,其因就是因为一个数可以变为二进制:
N = a 1 2 n + a 2 2 n − 1 + . . . + a n − 1 2 1 + a n 2 0 ( a i 只为 1 或 0 ) N=a_12^n+a_22^{n-1}+...+a_{n-1}2^1+a_n2^0(a_i只为1或0) N=a12n+a22n−1+...+an−121+an20(ai只为1或0)
上述观察我们发现,通过2的指数次方的跳跃,很轻易就可以达到N,故正常倍增时间复杂度可以达到 O ( l o g n ) O(logn) O(logn)。
但是,简单的跳跃并不能达到实际的效果,因此我们需要一个跳板 ,供其跳跃,很容易发现,跳板只有 l o g ( n ) log(n) log(n) 个,在实际问题中并不需要担心空间问题,需要担心的是跳板和跳跃的设计。
下图表示这一过程

注意 :跳跃并不是只能为2,或只能增长(一些时候可以成倍衰减,如:从8变为4再到2),这些都应与实际考虑。大多时候倍增为静态算法,面对动态问题时可能会吃力。另外更多时候倍增属于一种优化技巧,下文会提到。
例题
在下面我将用一道题来详细说明倍增算法实现过程
题意:多次询问,求最少区间数覆盖一段区间 ,无法覆盖就为-1
分析 :一道很明显的贪心覆盖区间问题,但是贪心法在这题的时间复杂度为 O ( n m ) O(nm) O(nm),直接TLE,所以得使用倍增优化。我们可以定义一个数组 d p i j dpij dpij表示从下标i向后跳 2 j 2^j 2j个区间到达的最远位置 ,这与上文对应的话就是以i为跳板,跳跃距离为 2 j 2^j 2j。之后,我们的问题就变为了如何求dp数组以及答案怎么表示。对于答案很简单,上文所说任何数都可以表示为2进制,这也意味着我们可以通过跳若干个2的任意次方步到达目标点(本题目标点为右端点r),且如果我们每次按最大步的跳的话,不会有重复时间复杂度也为 O ( l o g n ) O(logn) O(logn)。这一部分后续在题解时会给出操作流程。
关于为什么不会有重复跳跃:这可以基于二进制性质也可以用反证法,如果有重复步数,即两个 2 j 2^j 2j,那么一定可以转化为 2 j + 1 2^{j+1} 2j+1,这与上文所述每次跳最大步不符,矛盾,故不存在。
得出答案,我们可以全心全意进军处理dp数组了。
上文我说过,倍增是一种静态的方法,所以必须预处理。
对于一个 d p i j dpij dpij如何用已知量推出来?可以用到一个幂的性质
2 a = 2 a − 1 + 2 a − 1 2^a = 2^{a-1}+2^{a -1} 2a=2a−1+2a−1
到这一步,就可以看出来了,我们求得是i坐标跳跃 2 j 2^j 2j个区间到达得最远地方,是否意味着,我们也可以通过跳跃两个 2 j − 1 2^{j-1} 2j−1个区间达成相同的目的,所以,转移方程可以写为
d p i j = d p d p \[ i j − 1 j − 1 dpij =dpdp\[ij-1j-1 dpij=dpdp\[ij−1j−1
至此分析完毕。

题解 :我们维护一个dp数组和一个max_Rx数组表示x所在的所有区间中最大的右端点(这个数组其实就是贪心思路,不多赘述),max_R数组我们可以在读入区间 x , y x,y x,y时就可以预处理好:通过以下公式:
m a x R x = m a x ( m a x R x , y ) m a x R i = m a x ( m a x R i , m a x R i − 1 ) ; maxRx = max(maxRx, y)\\ maxRi = max(maxRi, maxRi - 1); maxRx=max(maxRx,y)maxRi=max(maxRi,maxRi−1);
关于预处理dp数组,不难发现, d p i 0 dpi0 dpi0就是max_Ri,那么根据上文转移方程式,可以轻松预处理出dp数组,但是,注意数组别开太大
最后求解答案部分,我们可以通过i位置开始递推,并设立一个pos表示下一步向后跳跃 2 p o s 2^{pos} 2pos的距离,如果没有跳过头(设定下一步跳的点一定小于右端点),就一直跳,但是这种情况最后一定还能再跳一步,最后判断,求解。
本题结束,下面放出代码(当然可能有些原因,代码不像其他大佬的一样简短,不过思路应该是较优了, O ( n l o g n ) O(nlogn) O(nlogn)):
cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 10;
int n, m;
int dp[N][25]; //表示从下标i向后跳2^j个区间到达的最远位置
int max_R[N]; //表示x所在的所有区间中最大的右端点
void init() //预处理
{
int j = 0;
for (int i = 1; i < N - 5; ++i)
{
max_R[i] = max(max_R[i], max_R[i - 1]);
dp[i][0] = max_R[i];
}
dp[0][0] = max_R[0];
for (int i = 1; i < 20; ++i)
for (int j = 0; j < N; ++j)
dp[j][i] = dp[dp[j][i - 1]][i - 1];
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin >> n >> m;
int x, y;
for (int i = 0; i < n; ++i)
{
cin >> x >> y;
max_R[x] = max(max_R[x], y);
}
init();
int a, b;
while(m--)
{
cin >> a >> b;
long long t = 1;
for (int i = 19; i >= 0; --i)
{
if (dp[a][i] < b) //能跳
{
t += (1 << i); //结果是跳了2^i区间
a = dp[a][i];
}
}
//最终一步还要跳
if (dp[a][0] < b) cout << "-1\n";
else cout << t << "\n";
}
return 0;
}
倍增算法应用
ST表
ST表可以说是倍增算法为核心的典型的应用了,主要解决静态RMQ类问题,下文就是以求最小值为例,其它的种类的st表与最小值求解类似。
问题引入 :给出一个长为n的序列,每次询问一段区间的最小值。
分析 :如果直接暴力求解的话,明显超时,如何优化?我们再次引入 d p i j dpij dpij表示从第i位往后跳 2 j 2^j 2j个单位的区间的最小值,那么我们发现就会有许多这样的区间,我们就可以选择其中几个来做区间覆盖,但是要保证正好覆盖。先来看怎么求解dp数组,其实与上面的例题类似,都是运用二进制或幂的知识。我们可以通过i跳两个 2 j − 1 2^{j-1} 2j−1的位置到达 2 j 2^j 2j的位置,这样整个 i , i + 2 j i,i+2\^j i,i+2j的区间都被我们给覆盖了,那么我只需要知道跳的那两步 2 j − 1 2^{j-1} 2j−1的最小值,就是 d p i j dpij dpij的最小值。综上,转移方程式应为:
d p i j = m i n ( d p i j − 1 , d p i + ( 1 \< \< ( j − 1 ) j − 1 ) dpij=min(dpij-1,dpi+(1\<\<(j-1)j-1) dpij=min(dpij−1,dpi+(1\<\<(j−1)j−1)
好了,在求最终答案部分,我们也需要覆盖区间,那么可以分割成若干个二进制来求(就向上文例题那样),但是也有更优的 O ( 1 ) O(1) O(1)求法,只需要两个区间,即左端点为其中一个的跳跃起始点,右端点为另一区间的末端点,那么这两个区间的另外几个点怎么求呢?可以用2的指数推回去,可该怎么选择指数使得区间正好覆盖又不会过头呢?其实这个值一定等于 l o g 2 ( l e n ) log_2(len) log2(len),len为区间长度,接下来给出证明:对于一个长度为len的区间,它一定为2的指数或非2的指数,对于是2的指数,那是显而易见的 2 l o g 2 ( l e n ) = l e n 2^{log_2(len)}=len 2log2(len)=len即区间全覆盖,对于非2的指数, l o g 2 ( l e n ) log_2(len) log2(len)在利用c++整数向下取整的特性时,一定有 2 l o g 2 ( l e n ) < l e n 2^{log_2(len)}<len 2log2(len)<len又因一定有整数a满足 2 a < l e n < 2 a + 1 2^a<len<2^{a+1} 2a<len<2a+1根据c++特性 l o g 2 ( l e n ) = a log_2(len)=a log2(len)=a,所以式子可以表示为如下 2 l o g 2 ( l e n ) < l e n < 2 l o g 2 ( l e n ) + 1 2^{log_2(len)}<len<2^{log_2(len)+1} 2log2(len)<len<2log2(len)+1
因为 2 l o g 2 ( l e n ) 2^{log_2(len)} 2log2(len)一定等于 2 l o g 2 ( l e n ) + 1 2^{log_2(len)+1} 2log2(len)+1的一半,所以其一定大于len的一半,以此实现两个区间的全覆盖,又因为最值保证了答案不会被重复区间所影响。
分析完毕!
解法 :依据上述分析其实可以直接写了,不过log2函数我们可以替换成手写的更加稳妥,主要看个人喜好。在解法部分主要讲的是答案的步骤公式,我们可以将其变成 d p i k dpik dpik与 d p j − ( 1 \< \< k ) + 1 k dpj-(1\<\
下面给出部分代码:
cpp
int a[N], LOG2[N];
int dp[N][25];
void init()
{
LOG2[0] = -1;
for (int i = 1; i <= n; ++i) dp[i][0] = a[i];
for (int i = 1; i <= n; ++i) LOG2[i] = LOG2[i >> 1] + 1; //手写log2
for (int i = 1; i <= 20; ++i)
{
for(int j = 1; j <= n; ++j)
{
dp[j][i] = min(dp[j][i - 1], dp[j + (1 << (i - 1))][i - 1]);//转移方程式
}
}
}
//以下为求解过程
while(m > 0)
{
--m;
cin >> x >> y;
int k = LOG2[y - x + 1]; //log2(len)
cout << min(dp[x][k], dp[y - (1 << k) + 1][k]) << "\n";
}

其实对于求最大值的st表只需要将转移方程和求结果的min改为max,求最大公约数只需要改为gcd(),原理几乎一样,不多赘述。
注意:st表只能解决静态区间查询,动态的仍需要线段树等算法解决。
LCA
(由于篇幅原因且LCA详解牵扯太多,这里只简单说明倍增的做法,详细可以搜索其他大佬的作品)
首先,最近公共祖先我们肯定不能一步一步找,否则会超时,那么我们就可考虑树上倍增,在树上"跳跃",以此来寻找最近公共祖先。我们可以假设一个数组 f a i j faij faij表示树上的一个节点i向上跳 2 j 2^j 2j步所到达的格子,这种的转移其实非常好写,经过st表和例题的经验,明显看出 d p i j = d p d p \[ i j − 1 ] j − 1 dpij = dpdp\[ij - 1]j - 1 dpij=dpdp\[ij−1]j−1
相对而言这是简单的。在寻求答案时,我们也只需要不断从小到大跳跃,最终找到答案,需要注意,这必须要保证这两个点在同一树的高度,否则倍增会跳错位,影响答案。
最后给出代码
cpp
void init()//预处理
{
for (int i = 1; i <= 19; ++i)
for (int j = 1; j <= n; ++j)
dp[j][i] = dp[dp[j][i - 1]][i - 1];
}
int LCA(int a, int b)
{
if (depth[a] < depth[b]) swap(a, b);
if (depth[a] != depth[b])//出现不同层情况
for (int i = 19; i >= 0; --i)
if (depth[dp[a][i]] >= depth[b])
a = dp[a][i];
if (a == b) return a;
for (int i = 19; i >= 0; --i)//开始跳跃
if (dp[a][i] != dp[b][i])
a = dp[a][i], b = dp[b][i];
return dp[a][0];
}
其它优化
倍增算法应算作比较优秀的优化算法,其可以优化dp(st表其实就是倍增优化dp),还有快速幂的原理也是倍增,还有很多倍增优化的题目。
总结
倍增算法的核心逻辑就是成倍增长 ,具体操作过程就是跳跃 和跳板 ,以此可以在 O ( n l o g n ) O(nlogn) O(nlogn)左右的时间(通常来说是这样的)来解决问题,其优点就是高效,代码简短,但是缺点是(缺点需按情景来说,如RMQ类问题的缺点就是只能解决静态问题 )。最后需要特别注意,倍增的通常套路是上述 2 a = 2 a − 1 + 2 a − 1 2^a=2^{a-1}+2^{a-1} 2a=2a−1+2a−1的这个公式,但是不代表全部都是这类套路(如3倍增长)也不一定是增长,也可能是缩小,这些都是要因题而定的。正常情况下,我们其实只需要因需跳跃 再结合进制 ,幂的知识,就可以得出思路来了。