倍增算法思想(二分新姿势--倍增法)

建议先看看这篇博文:https://blog.csdn.net/JarjingX/article/details/8180560【白话系列】倍增算法 博主:JarjingX

以下转自:https://blog.csdn.net/wybooooooooo/article/details/80778807

博主:wybooooooooo的博客

 当我们想要知道从最快地从A走到B时,朴素的想法是找出任何一个点走任意步会到达的地方,但是这样太耗内存。

但是实际上可以只记录走1,2,4,8,16步能到达的地方

从A出发:若跳8个格子(超过B了,放弃)

                  若跳4个格子(超过B了,放弃)

                  若跳2个格子(没超过B,跳吧)

                  若跳1个格子(没超过B,跳吧)

        从B出发:…………

        多么轻松的事情,只要一本很薄的小抄就可以了,最关键的是:它绝对不会连着跳两步都是跳相同的格子数,因为如果跳两次2个格子都是可行的话,那么它干嘛不跳4个格子捏?

从A出发跳1步到1(记录下来)

        从1出发跳1步到2(记录下来)

        …………(跳1步的记录完毕)

        从A出发跳2步?就是从A出发跳1步再跳1步的到的地方,翻看小抄,直接誊写从1出发跳1步会到的2这个格子作为A跳2步会到的格子。

        从1出发跳2步?跟刚才一样,直接誊写。

        …………(跳2步的记录完毕)

        从A出发跳4步?你还真去跳4步?不,它也就是等于从A出发跳2步到的2号点再跳2步会到的格子,那么我们直接誊写2号格子跳2步会到的格子就可以了。

以下转自:http://jiayuzun.coding.me/2016/08/05/bz-template/

算法理论

  • 朴素算法:记录下每个节点的父亲,使节点u,v一步一步地向上找父亲,直到找到相同的“祖先”,即是
    所求的答案,时间复杂度O(n)
  • 优化算法(倍增法):利用二进制的思想,想办法使一步一步向上搜索变成以2^k的向上跳。所以
    定义一个f[][]数组,使f[j][i]表示节点i的2^j倍祖先。

算法实现

1.预处理出所有节点的深度和父节点

  1. * BFS防止爆栈 无法处理孩子个数

  2. * DFS可能会爆栈 可以处理孩子个数 使用时建议扩栈

2.处理各节点的所有祖先节点

3.将所查询的两点上升到同一高度

  1. * 找到祖先(以2^k的高度向上找)

  2. * 未找到祖先,同时上升高度至找到公共祖先


附加代码:

定义及初始化

#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

猜你喜欢

转载自blog.csdn.net/blaze003003/article/details/81084954
今日推荐