精华内容
下载资源
问答
  • 奇妙的倍增法

    2019-05-26 09:13:58
    写这篇博客是因为做某一题的时候用到了倍增法,发现了倍增法的强大之处。 ...最大元素是什么。 我用倍增法做得,后来想想树链剖分+线段树应该也可以 谈到倍增法的好处,就是它:记录(建树)是...

    写这篇博客是因为做某一题的时候用到了倍增法,发现了倍增法的强大之处。

    https://www.lydsy.com/JudgeOnline/problem.php?id=4568

    意思不麻烦,2e4个点有点权,组成一棵树,2e5个询问,问u到v路径上的点组成的线性基可获得的

    最大元素是什么。

    我用倍增法做得,后来想想树链剖分+线段树应该也可以

     

    谈到倍增法的好处,就是它:记录(建树)是Onlogn的,查询也是Onlogn的

    并且还可以方便地记录树上的长段信息

    这样貌似很多问题都可以用倍增法解决了,比如这次的线性基

    再比如要查询任意路径长度?(这不也是树链剖分的基本操作嘛)

     

    不过好像倍增法不容易修改,貌似倍增法=树状数组  树链剖分=线段树

    展开全文
  • LCA--倍增法

    2019-03-25 23:30:00
    一般来求LCA有3种方法 ...(我也不明白假期学长讲的时候我为什么死活都不明白  自闭qwq  对不起学长qwq  明明学长讲的最好的qwq  想学长了qwq) --------------------------------------...

    一般来求LCA有3种方法

    1.倍增

    2.RMQ+欧拉序

    3.tarjan(离线)

     

    本文将倍增求lca

    这个算法是很常见很常见

    也是较好理解的

    (我也不明白假期学长讲的时候我为什么死活都不明白

     自闭qwq

     对不起学长qwq

     明明学长讲的是最好的qwq

     想学长了qwq)

     

    ---------------------------------------------------------------------------

     

    一、基础概念

    LCA定义:

      LCA(Lowest Common Ancestors),即最近公共祖先,是指在有根树中,找出某两个结点u和v最近的公共祖先。

     

    如图,结点4,6的公共祖先有1、2,但最近的公共祖先是2,即Lca(4,6) = 2

     

     

    显然遇到这个问题

    首先会想到暴力

    再一算

    时间复杂度"仅仅"是O(n)而已啦

    可是如果有q次询问呢

    比如说洛谷板子题

    n和q(这里的q即板子题中的m都是5 * 105的级别的

    那么n*q就是一个不小的数了

    那么暴力一定是过不了的了

    那么就只能优化优化了

     

     

    --------------------------------------------------------------------

     

    倍增法:

     

    注意到u,v走到最近公共祖先w之前,u,v所在结点不相同。

    而到达最近公共祖先w后,再往上走仍是u,v的公共祖先,即u,v走到同一个结点,这具有二分性质。

    于是可以预处理出一个2k的表,

    fa[k][u]表示u往上走2k步走到的结点,令根结点深度为0,则2k>depth[u]时,令fa[k][u]=-1(不合法情况的处理)

     

     

    不妨假设depth[u] < depth[v]

    ①将v往上走d = depth[v] - depth[u]步,此时u,v所在结点深度相同,该过程可用二进制优化。

    由于d是确定值,将d看成2的次方的和值,d = 2k1 + 2k2 + ... + 2km,利用fa数组,如v = fa[k1][v],v = fa[k2][v]加速。

    ②若此时u = v,说明Lca(u,v)已找到

    ③利用fa数组加速u,v一起往上走到最近公共祖先w的过程。

    令d = depth[u] - depth[w],虽然d是个未知值,但依然可以看成2的次方的和。

    从高位到低位枚举d的二进制位,设最低位为第0位,若枚举到第k位,有fa[k][u] != fa[k][v],则令u = fa[k][u],v = fa[k][v]。

    最后

    最近公共祖先w = fa[0][u] = fa[0][v],即u和v的父亲

     

     

     

     

     

    如何预处理?
        k=0时,fa[k][u]为u在有根树中的父亲,令根结点fa[k][root]=-1。

     k>0时,fa[k][u]=fa[k-1][fa[k-1][u]]。树的高度最多为n,k是logn级别。

     

    时间复杂度?
        预处理O(nlogn)
        单次查询O(logn)

     

    来个板子题也很好理解呀(传送门

    上面提到过的

    罗姑上也有板子题

    但是我最近是由hdu的oj写的

    正好还有那个的代码就直接拿过来了

     

     

    #include<cstdio>
    #include<iostream>
    #include<cstring>
    #define maxn 40000
    using namespace std;
    
    struct EDGE
    {
        int nxt,to,v;
    }edge[maxn*2+5];
    //链式前向星(可以用结构体,也可以用独立的数组) 
    //注意这里结构体的大小,下面会说的 
    
    int T,n,root,cnt,m;
    //T次模拟,cnt为边的个数(边的编号),n个点的树,m次询问,root是树的根(有的lca中没有给出根节点的标号,需要自己找,这里就没给) 
    int head[maxn+5],dep[maxn+5],dis[maxn+5];
    //head也是属于链前中的内容,写在这里因为[]中为点的标号,所以它的大小就是节点数的大小
    //dep存每个节点的深度,dis存每个节点到根节点的深度 
    int f[maxn+5][25];//前面的[i]为i节点 ,后面的[j]为从i节点往上跳2^j个节点 
    bool vis[maxn+5];//用来找根节点特意开的一个数组 
    
    void add(int x,int y,int z)//加边(用链式前向星存边) 
    {
        edge[++cnt].to=y;
        edge[cnt].v=z;
        edge[cnt].nxt=head[x];
        head[x]=cnt;
    }
    
    void dfs(int u,int fa)
    {
        dep[u]=dep[fa]+1;//深度加一 
        for(int i=0; i<=22; i++)//预处理,处理出u节点蹦2^i个节点所到达的节点编号 
        {
            f[u][i+1]=f[f[u][i]][i];
        }
        for(int i=head[u]; i; i=edge[i].nxt)//遍历他的出边(dfs) 
        {
            if(edge[i].to==fa)
            {
                continue;
            }//(因为正反都加边了)如果这个边指向的点是u节点的父节点,不dfs这个 
            dis[edge[i].to]=dis[u]+edge[i].v;//距离 
            f[edge[i].to][0]=u;//父节点 
            dfs(edge[i].to,u);
        }
    }
    
    int LCA(int x,int y)
    {
        if(dep[x]<dep[y])
        {
            swap(x,y);
        }//使x是最深的那个点 
        for(int i=22; i>=0; i--)
        {
            if(dep[f[x][i]]>=dep[y])//如果蹦的话x的深度仍比y的深度深,那就蹦,否则不蹦 
            {
                x=f[x][i];
            }
            if(x==y)//如果x蹦到了y,那么此时的x(也就是y)就是lca 
            {
                return x;
            }
        }
        //此时x和y的深度相同,但不是同一个节点 
        for(int i=22; i>=0; i--)
        {
            if(f[x][i]!=f[y][i])//如果x和y蹦完了到了相同的节点,那么蹦到的节点一定大于或等于lca 
            {//所以只有蹦到了不同的节点,才可以蹦(这样才能保证没蹦超出lca) 
                x=f[x][i];
                y=f[y][i];
            }
        }
        return f[x][0];//离真正的lca只差一步(此时不管蹦多少都超,所以前面的循环只能使x是离lca只差一层的节点) 
    }
    
    int main()
    {
        scanf("%d",&T);
        while(T--)//T次模拟(每次询问都是用新的树,因此要把之前的数组全部清空) 
        {
            memset(vis,0,sizeof(vis));
            memset(edge,0,sizeof(edge));
            memset(f,0,sizeof(f));
            memset(dep,0,sizeof(dep));
            memset(head,0,sizeof(head));
            memset(dis,0,sizeof(dis));
            cnt=0;
            scanf("%d%d",&n,&m);
            for(int i=1; i<=n-1; i++)
            {
                int x,y,z;
                scanf("%d%d%d",&x,&y,&z);
                vis[y]=1;
                add(x,y,z); 
                add(y,x,z);//因为不知道x和y谁是爸爸,所以就正反都加边 ,所以结构体的大小要开两倍 
            }
            for(int i=1; i<=n; i++)
            {
                if(vis[i]==0)
                {
                    root=i;
                    break;
                }
            }//没有被标记过的是根节点 
            dfs(root,0);
            for(int i=1; i<=m; i++)
            {
                int a,b;
                scanf("%d%d",&a,&b);
                printf("%d\n",dis[a]+dis[b]-2*dis[LCA(a,b)]);
            }
        }
        return 0;
    }

     

    转载于:https://www.cnblogs.com/darlingroot/p/10597611.html

    展开全文
  • LCA,最近公共祖先,这个东西有很多作用,因此,如何高效求出LCA就成了一个热点的...先来了解下什么是倍增吧,倍增其实就是二分的逆向,二分逐渐缩小范围,而倍增是成倍扩大。这里的倍增借用二进制来表达更容易理解;

    LCA,最近公共祖先,这个东西有很多作用,因此,如何高效求出LCA就成了一个热点的讨论话题。

    这里写图片描述

    下面所有的讨论都以图中这棵树为例子。

    先来了解下什么是倍增吧,倍增其实就是二分的逆向,二分是逐渐缩小范围,而倍增是成倍扩大。这里的倍增借用二进制来表达更容易理解;倍增的做法是先求出20,21,22,,然后任意一个数字都可以用20,21,22,相加来表示,就像给你32个1,你能表示出32-bit 中的任意一个二进制一样。

    倍增有什么好处呢,好处就是!倍增是一种优化手段,能提升查找等操作的效率的手段,其提升效率的原因就是二进制思想,提升的幅度为O(n)O(logn),具体的解释可以参照树状数组简单易懂的详解,数组数组的思想就是基于倍增来实现的。

    这里为什么要说成树上倍增呢?因为这个算法的操作都是在树上完成的,没错,求LCA的方法还有很多,比如RMQ-ST算法也可以做,这个算法的思想也是倍增,只不过这个倍增体现在区间上,而树上倍增法求最近公共祖先LCA的倍增体现在树的深度上。

    先说说朴素的做法,求两个结点的最近公共祖先,我会让一个结点先向上走到根,并记录下它走的路径,然后然让另一个结点也向上往根走,边走边在先前记录的路径中查找是否存在该结点。举个例子,求lca(3, 6),先让3走到1,路径为3, 2, 1,然后让6走到16在序列[3, 2, 1]中查找,没有找到,继续走,走到44在序列[3, 2, 1]中查找,没有找到,继续走,走到22在序列[3, 2, 1]中查找,找到了,那么lca(3, 6) = 2

    分析下朴素做法的时间复杂度,算法中需要让两个结点依次走到根,且在一个结点移动的过程中还需要在路径序列中查找;假设树有n个结点,由于树可能退化成链,因此从某一个结点移动到根这个操作的时间复杂度为O(n),而查找这个操作可以使用set这一类容器,故时间复杂度为O(logn),因此朴素算法求一次LCA的时间复杂度为O(nlogn),假设要多次求LCA,这个时间复杂度显然是不能接受的。

    然而,受剑指Offer66题之每日6题 - 第六天中第六题:两个链表的第一个公共结点 的启发,在树上求LCA和在两个链表的第一个公共结点是一样的,因此,朴素做法就有三种了,大家可以去剑指Offer66题之每日6题 - 第六天详细了解,这里就不多赘述了。

    LCA用得普遍的地方就是求树中两个结点之间的最短路:dis[u, v] = dis[root, u] + dis[root, v] - 2 * dis[root, lca(u, v)]


    现在就来好好说下树上倍增法求最近公共祖先LCA的算法了。

    思想

    算法的思想很简单,同剑指Offer66题之每日6题 - 第六天中第六题两个链表的第一个公共结点中的O(n)的做法一样,把两个结点移动到同一高度,然后一起向根走,一边走一边比较两个结点是否相等就行了。

    但是这样做,时间复杂度还是O(n),问题的规模较大时,复杂度还是不能接受,因此,树上倍增就是来提升这个效率的,树上倍增把移动这个操作提速了,原来只能一步一步移动,现在可以移动多步了。

    具体是怎么移动的呢?请看完预处理,然后接着看LCA就知道了。

    预处理

    首先,要预处理出数中每一个结点的深度dep以及到根的距离dis,前面也提到了,树上倍增是树深度的倍增,自然需要每一个结点的dep

    然后,要预处理出每一个结点的第2i个祖先pd[u][i],什么意思呢,举个例子就明白了,例如结点11的第20=1个祖先是9,第21=2个祖先是8,第22=4个祖先是1。这一步就是要为倍增提供”零件“。

    第一步可以使用dfs预处理出来,第二步,可以使用动态规划处理出来,pd[u][i] = pd[pd[u][i - 1]][i - 1],画个图就理解了。

    这里写图片描述

    结点C的第22=4个祖先等于结点C的第21=2个祖先B的第21=2个祖先A。

    LCA

    预处理完成后,剩下的事情就是向根结点移动了;

    第一步求出两个结点之间的高度差,让较深的那个结点移动到另一个结点一样的高度上,如果是朴素算法需要一步一步移动,而树上倍增算法把这个高度差表示成二进制,从而把这个移动转化成二进制的数位上移动,这样子,复杂度一下子就降到了O(logn)。举个例子,高度差diff = 6(110),那么较深的结点先移动2,这时高度差变为4,然后较深的结点移动4,这时两个结点的高度一样了。

    第二步就是两个结点同时向根移动,先看看两个结点最远的祖先是否相同,如果相同,说明最近的祖先还可能没出现,于是再看看两个结点第二远的祖先是否相同;如果两个结点最远的祖先不相同,说明这两个结点正在接近最近公共祖先,故把这两个结点同时移动到对应的祖先处。以此类推,最终可以得到最近公共祖先。这里距离都是2i,原因在第一步中已经说明。

    代码

    宏,全局变量

    /**
     * 直系祖先,pd[u][0]
     */
    #define NUM_PARENT 0
    /**
     * 树中结点的最大数目
     */
    #define MAXSIZE (40000 + 5)
    
    /**
     * 求二进制中最高一位1的index
     */
    #define BITOFBINARY(x) ((int)(log((x) * 1.0) / log(2.0)))
    
    /**
     * 求二进制中最低一位1所表示的数值
     */
    int lowbit(int x)
    {
        return x & -x;
    }
    
    /**
     * 树高的最大幂次
     */
    const int MAXDEP = BITOFBINARY(MAXSIZE);
    
    /**
     * 每个结点的深度,距根结点的距离
     */
    int dep[MAXSIZE], dis[MAXSIZE];
    
    /**
     * 每个结点的不同深度幂次的祖先
     */
    int pd[MAXSIZE][MAXDEP + 1];
    

    预处理

    /**
     * 求出每个结点的深度,距离根的距离及它们的直系祖先
     */
    void init_dfs(int src)
    {
        for (int i = head[src]; i + 1; i = edges[i].next) {
            int to = edges[i].to;
    
            // 领接表建树,避免重复访问
            if (to == pd[src][NUM_PARENT])
                continue;
            dep[to] = dep[src] + 1;
            dis[to] = dis[src] + edges[i].val;
            pd[to][NUM_PARENT] = src;
            init_dfs(to);
        }
    }
    
    /**
     * 动态规划求出每个结点不同距离的祖先
     */
    void init_redouble()
    {
        for (int power = 1; power <= MAXDEP; ++power)
            for (int i = 1; i <= n; i++)
                pd[i][power] = pd[pd[i][power - 1]][power - 1];
    }
    

    LCA

    int lca(int x, int y)
    {
        // 始终保持x结点的深度较深
        if (dep[x] < dep[y])
            swap(x, y);
    
        // 求出高度差,并使x移动到同y一样的高度
        for (int diff = dep[x] - dep[y]; diff; diff -= lowbit(diff))
            x = pd[x][BITOFBINARY(lowbit(diff))];
    
        // 处理x和y是同一个结点或y是x的祖先这两种情况
        if (x == y)
            return x;
    
        // x和y一样的高度,同时移动x, y
        for (int i = MAXDEP; i >= 0; --i)
            if (pd[x][i] != pd[y][i])
                x = pd[x][i],
                y = pd[y][i];
        return pd[x][NUM_PARENT];
    }
    

    完整代码

    这里结合一个题目背景,HDU2586:How far away?,完整地给出代码。

    这个题目的意思是:给你n个点,n - 1条边的最小生成树,然后给你m次询问,每次询问树中任意两个结点之间的最短路。

    做法是随便令一个结点为根,然后用树上倍增的方法求lca,然后利用dis[u, v] = dis[root, u] + dis[root, v] - 2 * dis[root, lca(u, v)]可以求得答案。

    n达到了40000m达到了200,朴素做法或许行得通,但我没试过。

    #include <bits/stdc++.h>
    
    using namespace std;
    
    #define MAXSIZE (40000 + 5)
    #define NUM_PARENT 0
    
    #define BITOFBINARY(x) ((int)(log((x) * 1.0) / log(2.0)))
    
    typedef struct Edge Edge;
    
    struct Edge {
        int to, val;
        int next;
        Edge() {};
        Edge(int to, int val, int next = -1) :
            to(to), val(val), next(next) {}
    };
    
    int n, m;
    Edge edges[MAXSIZE * 2];
    int head[MAXSIZE];
    
    int lowbit(int x)
    {
        return x & -x;
    }
    
    void add_edge(int x, int y, int val, int i)
    {
        edges[i] = Edge(y, val, head[x]);
        head[x] = i;
    }
    
    const int MAXDEP = BITOFBINARY(MAXSIZE);
    
    int dep[MAXSIZE], dis[MAXSIZE];
    int pd[MAXSIZE][MAXDEP + 1];
    
    void init_dfs(int src)
    {
        for (int i = head[src]; i + 1; i = edges[i].next) {
            int to = edges[i].to;
            if (to == pd[src][NUM_PARENT])
                continue;
            dep[to] = dep[src] + 1;
            dis[to] = dis[src] + edges[i].val;
            pd[to][NUM_PARENT] = src;
            init_dfs(to);
        }
    }
    
    void init_redouble()
    {
        for (int power = 1; power <= MAXDEP; ++power)
            for (int i = 1; i <= n; i++)
                pd[i][power] = pd[pd[i][power - 1]][power - 1];
    }
    
    int lca(int x, int y)
    {
        if (dep[x] < dep[y])
            swap(x, y);
    
        for (int diff = dep[x] - dep[y]; diff; diff -= lowbit(diff))
            x = pd[x][BITOFBINARY(lowbit(diff))];
    
        if (x == y)
            return x;
    
        for (int i = MAXDEP; i >= 0; --i)
            if (pd[x][i] != pd[y][i])
                x = pd[x][i],
                y = pd[y][i];
        return pd[x][NUM_PARENT];
    }
    
    int main()
    {
        int T;
        for (scanf("%d", &T); T--; ) {
            int x, y, val;
            scanf("%d%d", &n, &m);
    
            int root = 1;
    
            memset(head, -1, sizeof(head));
            memset(pd, 0, sizeof(pd));
            dis[root] = 0;
            dep[root] = 1;
    
            for (int i = 0; i < 2 * (n - 1); i += 2) {
                scanf("%d%d%d", &x, &y, &val);
                add_edge(x, y, val, i);
                add_edge(y, x, val, i + 1);
            }
    
            init_dfs(root);
            init_redouble();
    
            for (; m--; ) {
                scanf("%d%d", &x, &y);
                printf("%d\n", dis[x] + dis[y] - 2 * dis[lca(x, y)]);
            }
        }
        return 0;
    }
    

    复杂度

    预处理中,init_dfs的时间复杂度为O(n)init_redouble的时间复杂度为O(nlogn),所以总的复杂度为O(nlogn)

    由于倍增算法把树上的移动转为在二进制数位上的移动,故单次lca的时间复杂度为O(logn),可以接受;

    展开全文
  • 倍增法求Lca(最近公共祖先)

    千次阅读 多人点赞 2018-07-16 10:51:09
    看标题便知道了, 这篇博客力求解决的问题求出一棵树的两个结点的最近公共祖先(LCA), 方法是倍增法. 那么什么是Lca呢? 它一棵树上两个结点向上移动, 最后交汇的第一个结点, 也就是说这两个结点祖先里离树根...

    一. 明确问题

    看标题便知道了, 这篇博客力求解决的问题是求出一棵树的两个结点的最近公共祖先(LCA), 方法是倍增法.

    那么什么是Lca呢?

    它是一棵树上两个结点向上移动, 最后交汇的第一个结点, 也就是说这两个结点祖先里离树根最远也是离他们最近的结点.

    什么是倍增法呢?

    此问题说的是用倍增法求解lca问题, 那么我们可以推测这种方法还可以解决其他的一些问题(不在当下讨论范围). 在学习的过程中, 我是这么理解的: 它是一种类似于二分的方法, 不过这里不是二分, 而是倍增, 以2倍, 4倍, 等等倍数增长

    一下没有理解倍增法没关系, 看后面的做法, 再结合前面, 前后贯通大概可以理解的七七八八.

    二. 思路引导

    下面的思路试图把过程模块化, 如果你不知道一个地方如何实现, 还请不要纠结(比如不要纠结于树的深度怎么求, 假设我们求好了树的深度)

    我们找的是任意两个结点的最近公共祖先, 那么我们可以考虑这么两种种情况:

    1. 两结点的深度相同.
    2. 两结点深度不同.

    算法实现来说, 第一种情况是第二种情况的特殊情况, 第二种情况是要转化成第一种情况的

    先不考虑其他, 我们思考这么一个问题: 对于两个深度不同的结点, 把深度更深的那个向其父节点迭代, 直到这个迭代结点和另一个结点深度相同, 那么这两个深度相同的结点的Lca也就是原两个结点的Lca. 因此第二种情况转化成第一种情况来求解Lca是可行的.

    现在还不知道如何把两个结点迭代到相同深度, 别急, 这里用到的是上面提到的倍增法.

    那么剩下的问题事就解决第一种情况了, 两个结点深度相同了. 怎么求他们的Lca呢?

    这里也是用了倍增法. 思路和上一步的倍增法是一样的, 不同之处有两点
    1. 这次是两个结点一起迭代向前的.
    2. 这次迭代停止的条件和上次不一样.

    OK, 现在无法理解上面的几个过程没关系,只要知道我们用递增法解决了上述的两个问题. 具体细节看下面分析.

    三. 整体框架.

    那么过了一遍上面的思路引导之后, 我们可以大概想一想怎么实现整个的问题了.

    其实用求Lca这个问题可以分为两块: 预处理 + 查询, 其中预处理是O(VlogV), 而一次查询是O(logV), V代表结点数量. 所以总时间复杂度为O(VlogV +QlogV). Q为查询次数

    其步骤是这样的:
    1. 存储一棵树(邻接表法)
    2. 获取树各结点的上的深度(dfs或bfs)
    3. 获取2次幂祖先的结点, 用parents[maxn][20]数组存储, 倍增法关键
    4. 用倍增法查询Lca

    步骤一

    用邻接表存储一棵树, 并用from[]数组记录各结点的父节点, 其中没有父节点的就是root.

    parents[u][]数组存储的是u结点的祖先结点.
    如parents[u][0], 是u结点的2⁰祖先结点, 即1祖先, 也即父节点. 在输入过程中可以直接得到.
    parents[u][1], 是u结点的2¹祖先结点,即2祖先, 也即父亲的父亲
    parents[u][2], 是u结点的2²祖先结点, 即4祖先, 也即(父亲的父亲)的(父亲的父亲), 也就是爷爷的爷爷.

    理解这个关系很重要, 这也是通过父亲结点获取整个祖先结点的关键. 现在可以先跳过.

    void getData()
    {
        cin >> n;
        int u, v;
        for (int i = 1; i < n; ++i) {
            cin >> u >> v;
            G[u].push_back(v);
            parents[v][0] = u;
            from[v] = 1;
        }
        for (int i = 1; i <= n; ++i) {
            if (from[i] == -1) root = i;
        }
    }

    步骤二

    获取各结点的深度, 可以用DFS或这BFS方法

    void getDepth_dfs(int u) // DFS求深度
    {
        int len = G[u].size();
        for (int i = 0; i < len; ++i) {
            int v = G[u][i];
            depth[v] = depth[u] + 1;
            getDepth_dfs(v);
        }
    }
    
    void getDepth_bfs(int u) // BFS求深度
    {
        queue<int> Q;
        Q.push(u);
        while (!Q.empty()) {
            int v = Q.front();
            Q.pop();
            for (int i = 0; i < G[v].size(); ++i) {
                depth[G[v][i]] = depth[v] + 1;
                Q.push(G[v][i]);
            }
        }
    }

    步骤三

    求祖先

    在步骤一里面我们讨论了parents数组的意义, 它存的是结点u的2次幂祖先, 从父亲结点开始. 为什么要存2次幂? 这就是倍增法的思想了, 我们进行范围缩小不是一步一步的, 那样太暴力了, 所以我们需要某个跨度, 让我们能够先跨越大步, 接近的时候在小步小步跨越, 这样可以大大节省时间.

    读者可能会疑惑, 先大步, 后小步, 可是我怎么知道什么时候该大步, 什么时候该小步呢? 难道不会不小心跨过头吗?

    其实不会的, 在代码实现上, 这样的跨越有条件约束, 是非常讲究的. 读者不必为此纠结, 不过要讲解也是十分费力不讨好的事情, 所以请读者认证推敲后面Lca函数的代码, 认真琢磨为什么是那样跨越, 其中真味自会品出. 最好是自己写几个例子, 模拟跨越的过程, 在结合现实意义去理解

    那么我们回到当前问题. 请看下面这个公式:

    parents[i][j] = parents[parents[i][j-1]][j-1]

    这是构造此数组的公式. 不难理解, 父亲的父亲就是爷爷, 爷爷的爷爷就是4倍祖先. 请读者结合现实意义去理解.

    void getParents()
    {
        for (int up = 1; (1 << up) <= n; ++up) {
            for (int i = 1; i <= n; ++i) {
                parents[i][up] = parents[parents[i][up - 1]][up - 1];
            }
        }
    }

    步骤四

    做完了前面O(VlogV)的预处理操作, 剩下的就是查询了, 一次查询O(logV)

    因此, 我们可以敏锐的想到: Lca算法适合查询次数比较多的情况, 不然, 光是预处理就花了那么多时间了. 所以说, 查询是我们享受成果的时候了.

    int Lca(int u, int v)
    {
        if (depth[u] < depth[v]) swap(u, v); // 使满足u深度更大, 便于后面操作 
        int i = -1, j;
        // i求的是最大二分跨度 
        while ((1 << (i + 1)) <= depth[u]) ++i;
    
        // 下面这个循环是为了让u和v到同一深度 
        for (j = i; j >= 0; --j) {
            if (depth[u] - (1 << j) >= depth[v]) { // 是>=, 因为如果<,代表跳过头了,跳到了上面. 
                u = parents[u][j];
            }
        }
    
        if (u == v) return u; // 刚好是祖宗 
    
        // u和v一起二分找祖宗
        for (j = i; j >= 0; --j) {
            if (parents[u][j] != parents[v][j]) {
                u = parents[u][j];
                v = parents[v][j];
            }
        }
        return parents[u][0]; // 说明上个循环迭代到了Lca的子结点 
    }
    • 首先把u调整到深度更大(或相同)的结点, 便于后面操作.

    • 然后获取最大跨度i, 所有的跨越都是从i开始的.

    • 再然后把u上升到和v一样的深度. 也就是我们前面讨论过的情况二转情况一.

    • 最后, 两个结点同时迭代, 直到找到Lca

    至此, 我们的问题就解决了.

    完整代码

    #include <iostream>
    #include <algorithm>
    #include <cstring>
    #include <queue>
    #include <vector>
    using namespace std;
    
    const int maxn = 10005;
    int parents[maxn][20], depth[maxn];
    int n, from[maxn], root = -1;
    vector<int> G[maxn];
    
    void init()
    {
        memset(parents, -1, sizeof(parents));
        memset(from, -1, sizeof(from));
        memset(depth, -1, sizeof(depth));
    }
    
    void getData()
    {
        cin >> n;
        int u, v;
        for (int i = 1; i < n; ++i) {
            cin >> u >> v;
            G[u].push_back(v);
            parents[v][0] = u;
            from[v] = 1;
        }
        for (int i = 1; i <= n; ++i) {
            if (from[i] == -1) root = i;
        }
    }
    
    void getDepth_dfs(int u)
    {
        int len = G[u].size();
        for (int i = 0; i < len; ++i) {
            int v = G[u][i];
            depth[v] = depth[u] + 1;
            getDepth_dfs(v);
        }
    }
    
    void getDepth_bfs(int u)
    {
        queue<int> Q;
        Q.push(u);
        while (!Q.empty()) {
            int v = Q.front();
            Q.pop();
            for (int i = 0; i < G[v].size(); ++i) {
                depth[G[v][i]] = depth[v] + 1;
                Q.push(G[v][i]);
            }
        }
    }
    
    void getParents()
    {
        for (int up = 1; (1 << up) <= n; ++up) {
            for (int i = 1; i <= n; ++i) {
                parents[i][up] = parents[parents[i][up - 1]][up - 1];
            }
        }
    }
    
    int Lca(int u, int v)
    {
        if (depth[u] < depth[v]) swap(u, v);
        int i = -1, j;
        while ((1 << (i + 1)) <= depth[u]) ++i;
        for (j = i; j >= 0; --j) {
            if (depth[u] - (1 << j) >= depth[v]) {
                u = parents[u][j];
            }
        }
        if (u == v) return u;
        for (j = i; j >= 0; --j) {
            if (parents[u][j] != parents[v][j]) {
                u = parents[u][j];
                v = parents[v][j];
            }
        }
        return parents[u][0];
    }
    
    void questions()
    {
        int q, u, v;
        cin >> q;
        for (int i = 0; i < q; ++i) {
            cin >> u >> v;
            int ans = Lca(u, v);
            cout << ans << endl;
            //cout << u << " 和 " << v << " 的最近公共祖先(LCA)是: " << ans << endl; 
        }
    }
    
    int main()
    {
        init();
        getData();
        depth[root] = 1;
        getDepth_dfs(root);
        //getDepth_bfs(root);
        getParents();
        questions();
    }
    /*
    9
    1 2
    1 3
    1 4
    2 5
    2 6
    3 7
    6 8
    7 9
    5
    1 3
    5 6
    8 9
    8 4
    5 8
    */
    展开全文
  • 题意: 给你n个点,m条边,进行c此询问,若询问的两点联通的,则输出两者的距离,否则输出Not connected。 首先判断是否连通,显然用并查集就可以愉快的解决。...我用的倍增法就LCA,如果你不会LCA。。emmm,LC
  • 倍增法求后缀数组

    2016-07-14 17:14:29
    前言SA一种很好处理字符串的工具。 举个栗子很快可以明白 假设我们要处理“aabbabb”这个字符串,最终将得到: 字符串 a a b b ...一些关键数组:- sa[i]:排名第i名的后缀子串在什么位置。-
  • 什么是LCA? LCA就是最近公共祖先的缩写,就是假如我们有下面的一个树。那么这个树上的10号结点与7号结点的LCA就是2号结点 暴力的思路 在讲正解之前我们先来讲讲如何用暴力来解决这个问题。因为倍增就是对暴力的...
  • 是什么时候A的来着... 我当时就心态爆炸... 现在来进行简单整理 ~~我发现想黈之前的博客非常难,因为之前写的博客都是什么**东西啊~~ 其实我本身来讲也能理解(疯狂为下次培训不好好整理找理由) 以为澡堂给的时间...
  • 昨天我们介绍了矩阵的快速幂(点我),相信大家对于矩阵快速幂都有一定的了解。大家也就知道快速幂对于...光看这个好像也想不出什么,那么我们换一个角度来思考这个问题。我们现在要让计算变得更快,对吧?那么怎么样才
  • 相信大家对于矩阵快速幂都有一定的...光看这个好像也想不出什么,那么我们换一个角度来思考这个问题。我们现在要让计算变得更快,对吧?那么怎么样才能变得更快呢?时间复杂度的定义,就是从运算的次数来衡量时间...
  • LCA:倍增

    2019-05-26 14:42:14
    lca 先给张图(声明luogu版权) lca是什么呢,就是在一棵树里,两个节点的最近公共祖先 比如说,在上图中,4和5的lca就是2,8和10的lca就是1(很好理解对吗) lca主要有这样一些解决的方法 ...倍增法 一个...
  • 必经点问题在考试中也算是出现过好几次了,之前都用了其他的蜜汁方法水过去,昨天终究还是用了什么灭绝树 感觉还是要总结一下必经点这么一个东西了 当然听说支配树可以完全搞定所有问题的,不过常数较大罢了,这...
  • 【补】[LCA]倍增版LCA

    2018-11-05 15:09:00
    咕咕咕 ...NOIP之前坑还是要填的 之后。。肯能就退役了wwwww LCA 众所周知, LCA有许多的求(比如暴力) ...像ST表一样,用一个数组下标表示 \(2^j\) 步后的父亲节点是什么 怎么用来求LCA? 先来说说任...
  • 图论——倍增求LCA

    2018-11-09 11:02:00
    在树上的算法,但是为什么我们把它归为图论呢? 因为它对图论太重要了,其实,树也图,任意二节点只有一条路径的图。 我们来看一下LCA的栗子: 这就是LCA,很好理解吧! 那问题来了,怎么实现求两点的LCA呢...
  • LCA求的三度升级

    2016-10-13 19:10:20
    1.RMQ做法: ... 轻重路径剖分基础:树链剖分(无需看剖分过程,重点搞清楚什么是轻重路径)  那么,具体过程可以类比剖分的query过程: void dfs1(int u){  dep[u]=dep[fa[u]]+1; int Max=0;
  • 什么是后缀数组(sa) 后缀数组就是将s的后缀按字典序...有两种方法可以求sa,但是最经济实惠(最简单)的是倍增法,这里我们只讨论倍增法。首先我们可以考虑倍增法,求出第i位开始二的次幂个字符的rank,显然可以利
  • 【模板】板子的集合

    2019-10-03 19:45:49
    1. 倍增法 首先我们要知道倍增是什么,倍增思想大概就是对一个具有单调性的操作,通过使用\(O(n \log_{2} n)\)的时间与空间对单调性进行以2的次方为基准的预处理,来使得每次操作的效率优化。 接下来讲一下LCA...
  • 后缀数组详解

    2018-02-06 11:36:00
    什么是后缀数组 后缀数组处理字符串的有力工具 —罗穗骞 个人理解:后缀数组让人蒙逼的有力工具! 就像上面那位大神所说的,后缀数组可以解决很多关于字符串的问题, 譬如这道题 注意:后缀数组并...
  • 倍增法 DC3 最长前缀 附录 倍增法 C++ 实现( hiho 1403 )通过 DC3算法 C++ 实现( hiho 1403 )通过 简介 后缀数组就是把一个文本串的所有后缀按字典序从小到大排放的数组。由于线性构造后缀树比较复杂...
  • (转)后缀数组讲解

    2019-11-18 20:49:45
    倍增法 过程详解 基数排序 height数组 经典应用 两个后缀的最大公共前缀 可重叠最长重复子串 不可重叠最长重复子串 POJ1743 本质不同的子串的数量 后记 回到顶部 什么是后缀数组 后缀数组处理...
  • Luogu1081 开车旅行

    2018-10-29 08:12:00
    显然这没有什么决策的,选择方案都固定的 这可以考虑倍增跳,每个位置上跳到的位置可以通过查前驱后继解决的 有两种方式:  一种平衡树搞,倒着做查完了插入  另一种先排序建一个链表,查完了删除...
  • 倍增法求后缀数组思路分析代码实现height 数组height 数组是什么?线性求 height 数组思路分析代码实现例题讲解例 1:LCP 询问题目大意思路分析例 2:Musical Themes题目大意思路分析例题 3:【NOI 2015】品酒大会...
  • 目录什么是后缀数组前置知识后缀基数排序倍增法过程详解基数排序height数组经典应用两个后缀的最大公共前缀可重叠最长重复子串不可重叠最长重复子串 POJ1743本质不同的子串的数量后记&amp;amp;amp;amp;amp;amp;...
  • 并不对劲的后缀数组

    2018-01-19 08:46:00
    至于第二种方法是什么,并不对劲的人并不知道,所以只说倍增。 考虑正常地比较两个字符串,都是从头比较到尾: 那么,如果把两个字符串都断成两半,并且已知每一段的排名,就相当于以第一段为第一关键字,第二段...
  • lca tarjan实现(vector版)

    2018-01-15 14:15:36
    首先本人一个嵶苟,向前星还不太熟,所以...倍增法前2种属于离线算法,后2种属于在线算法,所谓离线算法就是把要查询的点对全部储存起来,再一次性把所有输出,在打比赛的过程中:in1 out1 in2 out2 和 in1 in2 out
  • 最近花了挺长一段时间去研究了一下,总算勉强学会了用倍增法来实现后缀排序(据说还有一种更快的\(DC3\)法,但是要难得多)。 数组定义 首先,为方便起见,我们用后缀\(_i\)表示从下标\(i\)开始的后缀。(相信大家...

空空如也

空空如也

1 2 3
收藏数 48
精华内容 19
关键字:

倍增法是什么