zoukankan      html  css  js  c++  java
  • 最近公共祖先(Least Common Ancestors)

    题意:

    给定一棵有根树T,给出若干个查询lca(u, v)(通常查询数量较大),每次求树T中两个顶点u和v的最近公共祖先,即找一个节点,同时是u和v的祖先,并且深度尽可能大(尽可能远离树根)。通常有以下几种算法:

    • 在线算法,每次读入一个查询,处理这个查询,给出答案。
    • 离线算法,一次性读入所有查询,统一进行处理,给出所有答案。

    在线:

    倍增(基于二分搜索):

    基本思想就是让u和v同时走到同一高度,然后再一起一步步往上走。
    将父亲结点的父亲结点利用起来,依次计算,便可以得到从当前结点向上走2k步所到达的顶点,这样便有了k以内的点的所有信息,进行二分查找答案即可~
    预处理时间复杂度O(nlogn),查询时间复杂度O(logn)

    关键代码:

    首先预处理阶段

    //DFS预处理所有结点的深度和父节点
    void dfs(int v, int p, int d)
    {
        pa[0][v] = p;
        dept[v] = d;
        for(int i = head[v]; i != -1; i = edge[i].next){
            int u = edge[i].to;
            if(u == p) continue;
            dfs(u, v, d + 1);
        }
    }
    void init()
    {
        dfs(root, -1, 0);
        //预处理祖先,向上走2^i所到的结点
        for(int i = 0; i < maxm - 1; i++){
            for(int j = 1; j <= V; j++){
                if(pa[i][j] < 0) pa[i + 1][j] = -1;
                else pa[i + 1][j] = pa[i][pa[i][j]];
            }
        }
    }

    计算u和v的lca

    int lca(int u, int v)
    {
        //让u和v 向上走到同一高度
        if(dept[u] > dept[v]) swap(u, v);
        for(int i = 0; i < maxm; i++){
            if((dept[v] - dept[u]) >>i &1)
                v = pa[i][v];
        }
        if(u == v) return u;
    
        //二分搜索计算lca
        for(int i = maxm - 1; i >= 0; i--){
            if(pa[i][u] != pa[i][v]){
                u = pa[i][u];
                v = pa[i][v];
            }
        }
        return pa[0][u];
    }

    基于RMQ的算法:

    初始化过程O(nlogn),查询过程O(1)
    有根树处理的一个技巧就是将树转化为从根DFS标号后得到的序列。而这种算法的基本思想就是将树看成一个无向图,u和v的公共祖先一定在u和v之间的最短路上。
    算法分三步:

    • 首先DFS对结点从跟开始标号,用数组vs保存访问顺序,height记录深度。每条边恰好经过两次,因此一共记录了2n1个结点
    • 计算对于每个顶点首次出现子的下标,保存在id中。
    • 获取LCA(u,v)LCA(u,v)=vs[id[u]iid[v]中深度最小的i]

    预处理:

    void dfs(int u, int pre, int dept)
    {
        vs[cnt] = u;
        height[cnt] = dept;
        id[u] = cnt++;
        for(int i = head[u]; i != -1; i = edge[i].next){
            dfs(edge[i].to, u, dept + 1);
            vs[cnt] = u;
            height[cnt++] = dept;
        }
    }
    void init()
    {
        cnt = 1; //vs数组下标从1开始
        dfs(root, root, 0);
        st.init(2 * V - 1);
    }

    而最后一步属于RMQ(Range Minimum/Maximum Query),即区间最值查询问题,我们可以用线段树解决,也可以使用ST(Sparse Table)算法,在O(nlogn)时间内进行预处理,然后在O(1)时间内回答每个查询。
    预处理使用动态规划,设dp[i][j]是从i开始的2j个数中的深度最小的值的下标。则有状态转移方程:

    if(height[dp[i][j - 1]] < height[dp[i + (1<<(j - 1))][j - 1]])  
        dp[i][j] = dp[i][j - 1];
    else   
        dp[i][j] = dp[i + (1<<(j - 1))][j - 1];

    初始化:

     for(int i = 1; i <= n; i++)  dp[i][0] = i;
    

    查询:

    int query(int a, int b)
    {
       if(a > b) swap(a, b);
       int k = lg[b - a + 1] ;
       if(height[dp[a][k]] <= height[dp[b - (1<<k) + 1][k]])
            return dp[a][k];
       else 
            return dp[b - (1<<k) + 1][k];
    }
    
    

    离线Tarjan算法:

    讲的很好

    Tarjan算法是离线算法,基于后序DFS和并查集。
    算法从根节点root开始搜索,每次递归搜索所有的子树,然后处理跟当前根节点相关的所有查询。

    算法用集合表示一类节点,这些节点跟集合外的点的LCA都一样,并把这个LCA设为这个集合的祖先。当搜索到节点x时,创建一个由x本身组成的集合,这个集合的祖先为x自己。然后递归搜索x的所有儿子节点。

    所有子树处理完毕之后,处理当前根节点x相关的查询。遍历x的所有查询,如果查询的另一个节点v已经访问过了,那么x和v的LCA即为v所在集合的祖先。

    建树可以用数组写链表也可以用vector保存,而查询可以用矩阵保存,这样可以减少重复,也可以用链表的形式,将一个结点的查询连在一起。

    Tarjan关键代码:

    void LCA(int u)
    {
        ance[u] = u;
        vis[u] = 1;
        for(int i = head[u]; i != -1; i = edge[i].next){
            int v = edge[i].to;
            if(vis[v]) continue;
            LCA(v);//访问子树
            unite(u, v);//子树与当前结点合并
            ance[_find(u)] = u;//祖先为u
        }
        for(int i = h[u]; i != -1; i = query[i].next){
            int v = query[i].q;
            if(vis[v])  ans[query[i].index] = ance[_find(v)];
        }
    }

    //感觉这个ance数组完全可以不用~~

  • 相关阅读:
    17、静态链表
    16、约瑟夫问题
    15、循环链表
    9、插入排序
    14、企业链表
    13、单向链表
    12、顺序表的顺序存储结构
    11、归并排序
    10、快速排序
    原型模式
  • 原文地址:https://www.cnblogs.com/Tuesdayzz/p/5758701.html
Copyright © 2011-2022 走看看