LCA

完了,又忘了一个

LCA(Least Common Ancestors),即最近公共祖先,

是指这样一个问题:在有根树中, 找出某两个结点 u 和 v 最近的公共祖先。

解决这类问题,容易想到一个朴素暴力算法,给出节点 u,v,,首先对 u 进行回溯一直到根节点,并对途中的节点加上标记。

然后对 v 进行回溯,直到找到一个被标记的节点 T, 此时 T 即为 u,v 的 LCA。

此方法写起来很简单但时间复杂度太高,故只适合查询次数极少的时候。 以下为三种效率较高的方法

一、Tarjan 离线算法

Tarjan 算法是一个在图中寻找强连通分量的算法。

算法的基本思想为:任选一结点开始进行深度优先搜索 dfs(若深度优先搜索结束后仍有未访问的结点,则再从中任选一点再次进行)。

搜索过程中已访问的结点不再访问。搜索树的若干子树构成了图的强连通分量。

应用到 LCA 问题上,tarjan 基于并查集,他通过在深搜树的同时,从查询集合中计算公共祖先。

设 lca(u,v),u->v,路径中他们的 lca 就是他们所在支树的根。而深搜顺序是 t->u->t->v。

u 先被访问,u 被访问结束回到 t 的时候,便将 u,t 合并为 1 个集合,那这个时候 u 的祖先就是 t,当访问到 v 的时候,他们的 lca 就是 u 的祖先 t。

void tarjan(int u){
    vis[u]=1;
    fa[u]=u;
    for(int i=0;i<q[u].size();i++){//枚举 u 的询问
        int v=q[u][i];
        if(vis[v]) //若(u,v)的 v 访问过,则 uv 不再一个子树,LCA 就是 V 的祖先
          cnt[find(v)]++;
    }
    for(int i=0;i<g[u].size();i++){
        int v=g[u][i];
        if(vis[v])continue;
        tarjan(v);
        fa[v]=u;
    } 
} 

更多tarjan

二、 欧拉序 ST 表

遍历一颗树的同时记录每个节点的访问时间,得到的序列是DFS序。

而欧拉序与dfs序类似,只是在记录时有所不同。

每当访问完一个节点的子树,则需要返回一次该节点,然后记录第一次访问的时间和返回结束时间。

对于点对<u,v>,他们的LCA在u,v的第一次访问序号之间,且是深度最小的点。

于是得到如下算法:

1、 通过dfs遍历树,得到一个欧拉序。计 st[u]表示点u在欧拉序第一次出现的序号, 计 dep[u]表示点 u 的深度。

2、 查询点对<u,v>,即查询[st[u],st[v]]区间深度最小的点对于区间查询最小值可以用 ST 表来完成。

三、 倍增法

求出每个点深度,对于求点对<u,v>的 lca,首先将更深的点向上跳成深度一致,然后 两个点同时向上跳,跳到同一个点就是最近公共祖先。

但是暴力这样做效率过低,考虑通过倍增的思想来完成这一过程。

算法过程:

1) dfs 遍历树,记录每个点的深度以及父亲

2) 设 f[i][j]表示节点 i 的 2j祖先。

3)通过倍增同步跳的方式查询 LCA

#include<bits/stdc++.h>
using namespace std;
int f[100010][20],vis[100010],dis[100010],n,q,rt;
vector<int>g[100010];
void dfs(int x,int d){
    vis[x] = 1;
    dis[x] = d;
    for(int i = 1;(1<<i)<=dis[x];i++)
        f[x][i] = f[f[x][i-1]][i-1];
    for(int i = 0;i<g[x].size();i++){
        int v = g[x][i];
        if(vis[v])continue;
        f[v][0] = x;
        dfs(v,d+1);
    }
} 
int lca(int x,int y){
    if(dis[x]<dis[y])swap(x,y);
    int k = dis[x]-dis[y];
    for(int i = 0;(1<<i)<=k;i++)//让深度大的点 x 向上跳与 y 保持深度一致
        if(k&(1<<i))
            x = f[x][i];
    if(x==y)return x;
    for(int i = 19;i>=0;i--){
        if(f[x][i]!=f[y][i])
            x = f[x][i],y = f[y][i];
    }
    return f[x][0];
}
int main(){
    scanf("%d",&n);
    for(int i = 1;i<=n;i++){
        int u,v;
        scanf("%d%d",&u,&v);
        g[u].push_back(v);
        g[v].push_back(u);
    }
    dfs(1,0);
    scanf("%d",&q);
    for(int i = 1;i<=q;i++){
        int x,y;
        scanf("%d%d",&x,&y);
        int tmp = lca(x,y);
                printf("%d\n",dis[x]+dis[y]-2*dis[tmp]);
    }
    return 0;
}

综上所述,我是蒟蒻

2022-10-27 16:23:39

posted @ 2022-10-27 15:00  cztq  阅读(92)  评论(0编辑  收藏  举报