LCA的不同实现方法

题目描述

  • 给出一颗 \(n\) 个节点的树,\(Q\) 组询问,求两点的最近公共祖先
  • \(1\le n,Q\le 10^5\)

分析

对于每一组询问,显然两个之间有且仅有一条路径,且仅有一个最近公共祖先

算法1 (暴力)

\(dfs\) 两次,分别从两个节点 \(dfs\),寻找一个到两个节点距离之和最短的节点,即为最近公共祖先,时间复杂度 \(O(n)\)

算法2 (倍增)

倍增算法-变量的定义

\(dep_i\) -> 节点 \(i\) 的深度(默认以 \(1\) 为根)
\(fa_{i,j}\) -> 节点 \(i\)\(2^j\) 的祖先

倍增算法-实现

  • 对于 \(dep_i\)\(fa_{i,0}\) 可以通过一趟 \(dfs\) 实现
fa[1][0] = 1 ;
void dfs(int x) {
    for (int j = lnk[x] ; j ; j = nxt[j])
        if (son[j] != fa[x][0]){
            fa[son[j]][0] = x ;
            dep[son[j]] = dep[x] + 1 ;
            dfs(son[j]) ;
        }
}
  • 对于 \(fa_{i,j}\) 可以通过倍增在 \(O(nlog_2n)\) 的时间内求出
for (int j = 1 ; j <= 20 ; ++ j)
        for (int i = 1 ; i <= n ; ++ i)
            fa[i][j] = fa[fa[i][j - 1]][j - 1] ;
  • 知道了这两个数组,怎么求 \(LCA\)
  • 首先,将 \(x\)\(y\) 移动到同一层
  • 如果 \(x\)\(y\) 是同一个点,那么 \(LCA\) 就是这个点
  • 否则,枚举倒序 \(i\),判断 \(fa_{x,i}\)\(fa_{y,i}\) 是否是同一个节点
  • 如果不是,同时跳到 \(2^j\) 的祖先点上
  • 如果是,那么不跳
  • \(LCA\) 就是 \(x\)\(y\) 的父亲节点
  • 没看懂自行理解或看代码
int lca(int x, int y) {
    if (dep[x] < dep[y]) std :: swap(x, y) ;
    int res = dep[x] - dep[y] ;
    for (int i = 20; i >= 0; -- i)
        if ((res >> i) & 1)
            x = fa[x][i] ;
    if (x == y) return x ;
    for (int i = 20; i >= 0; -- i)
        if (fa[x][i] != fa[y][i])
            x = fa[x][i], y = fa[y][i] ;
    return fa[x][0] ;
}

算法3 (树链剖分)

不知道的可以跳过

树链剖分-变量的定义

\(dep_i\) -> 节点 \(i\) 的深度(默认以 \(1\) 为根)
\(fa_i\) -> 节点 \(i\) 的父亲
\(bel_i\) -> 树链剖分中 \(i\) 所在的链的祖先
\(sz_i\) -> \(i\) 的子树的大小
\(dfn_i\) -> \(i\)\(dfs\)

树链剖分-实现

  • 预处理根节点
fa[1]=1;
bel[1]=1;
dep[1]=1;
  • 预处理,求出 \(sz\) , \(fa\) 数组
void dfs0(int x){
    sz[x]=1;
    for (int j=lnk[x];j;j=nxt[j])
        if (fa[x]!=son[j]) {
            fa[son[j]]=x;
            dep[son[j]]=dep[x]+1;
            dfs0(son[j]);
            sz[x]+=sz[son[j]];
        }
}
  • 找重链,求出 \(dfs\)
void dfs(int x){
    dfn[x]=++ind;
    int k=0;
    for (int j=lnk[x];j;j=nxt[j])
        if (son[j]!=fa[x]&&sz[k]<sz[son[j]])
            k=son[j];
    bel[k]=bel[x];
    if (k) dfs(k);
    for (int j=lnk[x];j;j=nxt[j])
        if (son[j]!=fa[x]&&son[j]!=k){
            bel[son[j]]=son[j];
            dfs(son[j]);
        }
}
  • 对于每一次询问,查找最近公共祖先
  • 首先,如果它们属于同一条链,那么最近公共祖先必然是取 \(dep\) 小的
  • 否则,比较所在链头的深度,取深度大的往上跳,直到它们属于同一条链
int lca(int x,int y){
    while (bel[x]!=bel[y]){
        if (dep[bel[y]]<dep[bel[x]]) x=fa[bel[x]];
        else y=fa[bel[y]];
    }
    return dep[x]<dep[y]?x:y;
}
  • 时间复杂度 \(O(Qlog_2n)\)

算法4 (tarjan)

  • 以上的都是在线的
  • 现在要讲的 \(tarjan\) 是一种离线的算法

\(tarjan\)-变量的定义

\(e0\) -> 初始的图
\(e1\) -> 询问的图
\(u\) -> 当前的节点
\(ans_i\) -> 第 \(i\) 个询问的答案
\(vis_i\) -> 节点 \(i\) 是否已经被遍历了
\(fa_i\) -> 节点 \(i\) 最后合并到了哪个节点

\(tarjan\)-实现

  • 先搜索一趟 \(e0\) ,遍历整个图
  • 然后搜索一趟 \(e1\) ,查找与 \(u\) , 有关的询问
  • 若询问节点已经被访问过了,那么询问的结果就是询问节点的祖先
  • 最后将当前节点的祖先设为当前节点的父亲节点,并查集维护即可
void dfs(int u,int f){
    for (vector <int> :: iterator i=e0[u].begin();i!=e0[u].end();i++){
        int v=*i;
        if (v!=f) dfs(v,u);
    }
    vis[u]=1;
    for (vector <pii> :: iterator i=e1[u].begin();i!=e1[u].end();i++){
        int v=i->fi;
        if (vis[v]) ans[i->se]=get_father(v);
    }
    fa[get_father(u)]=get_father(f);
}
  • 时间复杂度 \(O(n\alpha+Q)\) , \(\alpha\) 是并查集系数
  • 如果可以不用在线的话, \(tarjan\) 是一个不错的选择

总结

  • 倍增是比较容易实现的算法,在一般数据不大的情况下还是不错的
  • 树链剖分比倍增快
  • \(tarjan\) 的速度最快,实现也比较简单,在询问特别多的情况下最好使用 \(tarjan\)
posted @ 2018-03-31 21:57 xay5421 阅读(...) 评论(...) 编辑 收藏