倍增
倍增在思想上和分治是类似的,主要利用的思想是"分而治之"。
分治
分治就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题。
直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。

归并排序
主要思想是:先将数组从中间拆分,然后两两继续拆分,直到每个部分都被分成了最小单位 (就是一个数)。
如下图:

二分是一种特殊的分治
有一类问题,我们很难直接从正面求解,但它的答案范围有边界,即具有有边界界性,且至少在一定范围内具有单调性(最多局部不变)。
这样,我们可以拓展一下分治思想,从答案这边二分,不断逼近正解,直到找到正解(整数)或逼近到精度内(实数)。
例题及解法
\(NOIP \ 2010\) 关押罪犯:二分+染色。
\(NOIP \ 2011\) 聪明的质检员:二分。
\(NOIP \ 2012\) 借教室:二分+前缀和。
遇到此类题需要认真思考,辨别二分答案特征,用二分答案更好地解决问题。
倍增
倍增,就是把一个数据规模为 \(n\) 的问题分解成若干个 \(2^{a_i}\) 的和,预处理数据范内所有 \(2^{a_i}\) 的情况,再将这些规模为 \(2^{a_i}\) 的问题通过一定的方法合并,得出原问题的解。
分治与倍增的相似之处:
- 分治是把整个问题分成几个互不重复的子问题,合并求解。
- 倍增是找互为倍数关系的子问题之间的联系,再合并求解。
所以我们可以认为:倍增也体现了分治思想。

从上图中不难看出,每次操作所分的长度是以 \(2\) 的指数倍进行增长的,从而让我们的操作更快。
常见倍增
(1)快速幂运算
基本的快速幂想必大家都掌握了,代码如下:
ll power (ll a, ll b, ll p) { //a^b%p
ll ans = 1%p;
while (b) { //如果b还有位数,执行循环,直到b==0
if (b&1) ans = ans*a%p; //若b在二进制表示下的最后一位是1,则ans加幂
a = a*a%p;
b >>= 1; //b右移1
}
return ans;
}
那么如何用倍增来优化快速幂呢?
举个例子:
我们将 \(a^{19}\) 次方分解,就会变成:\(a*a^2*a^{16}\),这里 \(a^{16}\) 次方是由 \(a^8\),\(a^4\),\(a^2\) 一点点累乘积累来的。
而看到 \(2^n\),我们就想到了二进制,即:\({(19)}_{10}={(10110)}_{2}\)
我们在转化二进制时就可以求出 \(a^2\),\(a^4\),\(a^8\),\(a^{16}···\) 分别等于多少。
所以对于 \(a^n\),我们有如下代码进行分解:
s=1;
while(n>0){
if(n%2==1) s=s*a;
a=a*a;
n=n/2;
}
这样快速幂的的实现可以采用下列代码:
int fast(int a,int n) {
int s=1;
while(n) {
if(n%2==1) s=s*a;
a=a*a;
n=n/2;
}
return s;
}
我们想要让代码更快,就可以使用位运算的优化手段。
- \(n \ \& \ 1\):取 \(n\) 的二进制最末位。
- \(n >> 1\):右移 \(1\) 位,相当于去 \(n\) 掉的二进制最末位。
优化后的代码:
int fast(int a,int n) {
int s=1;
while(n) {
if((n&1)==1) s=s*a;
a=a*a;
n=n>>1;
}
return s;
}
这里也提供一种运用分治的递归方法:
int fast(int a,int n){
if(n==1) return a;
int s=fast(a,n/2);
s=s*s;
if((n&1)==1) s=s*a;
return s;
}
(2) 区间倍增(ST表)
求区间的最值问题:
对于长度为 \(n\) 的数列 \(A\),回答 \(Q\) 个询问\(RMQ \ (A,i,j) \ (i,j \leq n)\),
返回数列A中下标在 \([i,j]\) 里的最小值或最大值。
\(ST\) 算法,可以做到 \(\Theta(n*logn)\) 的预处理
例如数列 \(a[10]\):
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|
| 4 | 1 | 3 | 5 | 6 | 3 | 8 | 7 | 9 | 12 |
设 \(f[i][j]\) 表示从 \(a[i]\) 开始连续 \(2*j\) 个元素中的最大值。
显然有 \(f[i][0]=a[i]\) 作为初始的边界状态。

如上图,我们可以得到状态转移方程:
\(f[i][j]=max(f[i][j-1],f[i+2^{j-1}][j-1])\)
假如我们要求区间 \([L,R]\) 中的最大值,最大值范围为 \([10,36]\)。

如图,我们只需分成等长 \(16\) 的两段即可。
所以可以得到:对于任意的 \([L,R]\),一定能拆分成不超过 \(log(R-L+1)\) 段
根据 \(2k \leq R-L+1\),得到最大的 \(k=(log(R-L+1)/log(2))\)。
区间就被我们分成了 \([L,L+2^k-1]\) 和 \([R-2^k+1,R]\) 两部分。
状态转移方程为:
\(Q[L,R]=max(f[L][k],f[R-2^k-1][k])\)
\(ST\) 表的实现
- 生成 \(ST\) 表:
void st(){
int k=log(n)/log(2);
for(int i=1;i<=n;++i){
f[i][0]=a[i];
}
for(int j=1;j<=k;++j){
for(int i=1;i+(1<<j)-1<=n;++i){
f[i][j]=max(f[i][j-1],f[i+(1<<(j-1))][j-1]);
}
}
}
- 查询区间 \([L,R]\) 中的最大值:
int rmq(int l,int r){
int k=log(r-l+1)/log(2);
return max(f[l][k],f[r-(1<<k)+1][k]);
}
(3)树上倍增(LCA)
考虑树上 \(u\) 和 \(v\) 两点间的路线问题。
\(LCA\),即最近公共祖先,是指在有根树中,找出某两个结点 \(u\) 和 \(v\) 最近的公共祖先。
记为:\(LCA(u,v)\)。
方法1:暴力求解
用 \(father[i]\) 记录 \(i\) 的父亲结点。
首先将 \(u\) 和 \(v\) 中深度较深的那个点跳到和较浅的点同样的深度。
然后两个点一起一步一步向上跳,直到跳到同一个点 \(p\),就是它们的 \(LCA\)。
复杂度:最坏情况 \(\Theta(n)\)。
适合只计算一次 \(LCA(u,v)\)。
实现方法:
\(u\) 和 \(v\) 深度大的向上走 \(|deep[u]-deep[v]|\)。
再一起一步一步向上走,直到走到同一个结点 \(p\)。
时间:\(\Theta(deep[u]+deep[v])\)。
这里我就直接贴求 \(LCA\) 的代码了:
int LCA(int u,int v) {
while(deep[u]>deep[v]) u=father[u];
while(deep[u]<deep[v]) v=father[v];
while(u!=v) {
u=father[u];
v=father[v];
}
return u;
}
方法2:利用倍增思想求LCA(u,v)
确定深度 \(deep[i]\),表示 \(i\) 到根节点的距离或者说是深度。
\(father[i][j]\) 表示 \(i\) 向上的第\(2*j\)个祖先。
\(1.\) \(dfs\) 求出\(deep[i]\)和 \(father[i][0]\)。
\(2.\) 预处理\(father[i][j]\):利用递推式 \(father[i][j]= father[father[i][j-1]][j-1]\)。
\(3.\) 如果 \(u=v\),\(LCA=u \ or \ v\)。
如果 \(u\) 和 \(v\) 的深度不同,则调整到同一深度,以便求 \(LCA\)。
当 \(u\) 和 \(v\) 的深度相同了,再开始向上走,每次两个点同时走相同的步数,在公共祖先中找最近的。
预处理代码:
void init(){
maxh=log(n)/log(2)+1;
for(int j=1;j<=maxh;++j){
for(int i=1;i<=n;++i){
father[i][j]=father[father[i][j-1]][j-1];
}
}
}
\(LCA\) 代码:
int LCA(int u,int v) {
if(depp[u]<depp[v]) {
swap(u,v);
}
int d=depp[u]-depp[v];
for(int i=0; i<=maxh; ++i) {
if(1<<i&d) {
u=father[u][i];
}
}
if(u==v) {
return 0;
}
for(int i=maxh; i>=0; --i) {
if(father[u][i]!=father[v][i]) {
u=father[u][i];
v=father[v][i];
}
}
return father[u][0];
}
结尾
对于倍增,我觉得只是一个编程技巧,它可以与很多算法搭配使用。而倍增延申的东西,还是很复杂的。
分治和倍增其实是用的一种方法,只是倍增的效率要比分治高,希望大家尽量掌握我所提的这些东西,这几个算法其实就是关于倍增的一些比较常用的算法。

浙公网安备 33010602011771号