建议先看看这篇博文:https://blog.csdn.net/JarjingX/article/details/8180560【白话系列】倍增算法 博主:JarjingX
以下转自:https://blog.csdn.net/wybooooooooo/article/details/80778807
倍增思想举例
A、B两点之间相隔若干单位为1的距离,如何从最快地从A走到B?
朴素的想法是:因为A B之间距离未知,只能从A开始试探性走1步、走2步、……看看走多少步能到达B,这样的时间复杂度是o(n)的。当然这样是不够高效(耗时间)。
实际上可以只记录走1,2,4,8,16步能到达的地方
从A出发:若跳8个格子(超过B了,放弃)
若跳4个格子(超过B了,放弃)
若跳2个格子(没超过B,可以跳)
若跳1个格子(没超过B,可以跳)
其中关键的思想是:它绝对不会连着跳两步都是跳相同的格子数,例如:如果跳两次2个格子都是可行的话,那么它干嘛不直接跳4个格子呢?依此类推。。。。。。
以下转自:http://jiayuzun.coding.me/2016/08/05/bz-template/
倍增求LCA
算法理论
- 朴素算法:记录下每个节点的父亲,使节点u,v一步一步地向上找父亲,直到找到相同的“祖先”,即是
所求的答案,时间复杂度O(n) - 优化算法(倍增法):利用二进制的思想,想办法使一步一步向上搜索变成以2^k的向上跳。所以
定义一个f[][]数组,使f[j][i]表示节点i的2^j倍祖先。
算法实现
1.预处理出所有节点的深度和父节点
-
* BFS防止爆栈 无法处理孩子个数
-
* DFS可能会爆栈 可以处理孩子个数 使用时建议扩栈
2.处理各节点的所有祖先节点
3.将所查询的两点上升到同一高度
-
* 找到祖先(以2^k的高度向上找)
-
* 未找到祖先,同时上升高度至找到公共祖先
附加代码:
定义及初始化
#pragma comment(linker, "/STACK:10240000000,10240000000")//扩栈,要用c++交,用g++交并没有什么卵用。。。
const int N = 100005;
int n , m , pre[N] , rt[N], mcnt;
//pre 邻接表数组 rt 求根节点 mcnt 邻接表下标变量
bool vis[N];
struct edge
{
int x , next;
} e[N << 1]; //邻接表
int dep[N] , f[17][N] , Lev , s[N];
//dep[]储存深度 1<<16 < N f[j][i] 表示i的第2^j个祖先 s[]孩子个数
void init()
{
memset(pre , -1 , sizeof(pre));
memset(rt, 0, sizeof(rt));
mcnt = 0;
}
算法函数
void addedge(int x, int y)//邻接表加边函数
{
e[mcnt].x = y,
e[mcnt].next = pre[x],
pre[x] = mcnt ++;
}
void dfs(int x , int fa)///也可以用bfs,但bfs不能统计节点孩子个数
{
dep[x] = dep[fa] + 1;
f[0][x] = fa , s[x] = 1;
for (int i = pre[x] ; i!=-1 ; i = e[i].next)
{
int y = e[i].x;
if (y != fa)
{
dfs(y , x);
s[x] += s[y];///节点x的孩子个数
}
}
}
// dfs处理后,要进一步处理得到节点的所有祖先
// for (j = 1 ; 1 << j < n ; ++ j)
// for (i = 1 ; i <= n ; ++ i)
// {
// f[j][i] = f[j - 1][f[j - 1][i]];
// }
// Lev = j - 1;
void bfs(int rt)///不需要求孩子个数,同时防止暴栈
{
queue<int> q;
q.push(rt);
f[0][rt] = 0, dep[rt] = 1, vis[rt] = 1;
while (!q.empty())
{
int fa = q.front();
q.pop();
for (int i = pre[fa] ; ~i ; i = e[i].next)
{
int x = e[i].x;
if (!vis[x])
{
dep[x] = dep[fa] + 1;
f[0][x] = fa , vis[x] = 1;
q.push(x);
}
}
}
}
int LCA(int x , int y)
{
if (dep[x] > dep[y])
{
swap(x , y);
}
for (int i = Lev ; i >= 0 ; -- i)///找y的第dep[y] - dep[x]个祖先
if (dep[y] - dep[x] >> i & 1)//dep[y]-dep[x]刚好比2的i次方大时
{
y = f[i][y];
}
if (x == y)
{
return y;
}
for (int i = Lev ; i >= 0 ; -- i)//同一高度后开始找祖先
if (f[i][x] != f[i][y])//不停的上次2的i次方,直到i==0
{
x = f[i][x] , y = f[i][y];
}
return f[0][x];
}
int get_kth_anc(int x , int k) ///找x的第k个祖先
{
for (int i = 0 ; i <= Lev ; ++ i)
if (k >> i & 1)
{
x = f[i][x];
}
return x;
}
倍增的其它应用
在好多算法中都用到了倍增法的思维,在这里总结一下,通过两个例子解释一下
1.快速幂
给出x,y,p,求x^y%p,如果x,y的数据很大的话,o(n)的算法会超时,那么这时候我们可以用倍增的方法减少运算次数
先求出
x^1 x^2 x^4 x^8.....(不过几十次运算)
对于任意一个y x^y 都可以由上面的项做乘积得到(也不过是几十次运算)
这样就大大减少了运算次数
2.RMQ求区间最值问题
给出n个数组成的数列,q次询问,每次给出x,y问x~y之间的最小值是多少?
如果直接暴力的话复杂度o(n*q)
RMQ算法也是用到了倍增的方法
f(i,1)表示从第i个位置开始,往后1个数的最小值
f(i,2)表示从第i个位置开始,往后2个数的最小值
f(i,3)表示从第i个位置开始,往后4个数的最小值
。。。。
则递推式即为f(i,k)=min(f(i,k-1),f(i+2^(k-2),k-1))
时间复杂度为o(n*logn+q)
【分治法】
先简略讲下分治法,分治法实现二分其实就是用决策区间折半,再判断是否可行,以修正区间的范围。
Ps:博主写的二分是求最小值的(求最大值自己yy一下就行了)
代码如下:
while (L<=R)
{
int mid=(R-L>>1)+L;
if (Check(mid)) R=mid-1; else L=mid+1;
}
- 1
- 2
- 3
- 4
- 5
答案:LL
【倍增法】
倍增法用了二进制的思想巧妙实现了二分。
对于区间[L,R],定义logR=log2(R)logR=log2(R)(Ps:log2(R)是求以2为底R的对数的函数)。
定义一个数ansans,初值为0。
每次二分检验ans+1<<logRans+1<<logR(Ps:先判断ans+1<<logRans+1<<logR是否在区间[L,R]范围内)是否可行,不可行则ans+=1<<logRans+=1<<logR。
每次二分检验让logRlogR逐渐减小(每次减1),这样相当于枚举了二进制的每一位。
正确性显而易见。
代码如下:
int logR=log2(R);
while (logR>=0&&ans+(1<<logR)>=L&&ans+(1<<logR)<=R)
{
if (!Check(ans+(1<<logR))) ans+=(1<<logR);
logR--;
}
- 1
- 2
- 3
- 4
- 5
- 6
答案:ans+1