【图论·树】最近公共祖先LCA

8.最近公共祖先LCA

一棵有根树,若节点z即是节点x的祖先,又是y的祖先,则称z是x、y的公共祖先,其中深度最大的一个被称为x、y的最近公共祖先。

理解LCA:

向上标记法:从点x向上走到根节点,并标记所有经过的节点。然后从点y向上走到根节点,遇到的第一个已标记的节点就是LCA(x,y)。单次复杂度\(O(N)\)

可以每次找深度比较大的那个点,让它向上跳。显然在树上,这两个点最后一定会相遇,相遇的位置就是想要求的 LCA。 (或者先向上调整深度较大的点,令他们深度相同,然后再共同向上跳转,最后也一定会相遇。)这种方法可以求出从点u到点v的简单路径上的点。单次复杂度\(O(dis(u,v))\)

LCA具有交换律和结合律。

8.1.重链剖分在线求LCA\(O(N+Q\log N)-O(N)\)

时间复杂度:\(O(N+Q\log N)\)。空间复杂度:\(O(N)\)

int dep[N],siz[N],fa[N],son[N];
int top[N];

void dfs1(int u)
{
    dep[u]=dep[fa[u]]+1,siz[u]=1;
    for(int i=h[u];i!=0;i=ne[i])
    {
        int v=e[i];
        if(v==fa[u]) continue;
        fa[v]=u;
        dfs1(v);
        siz[u]+=siz[v];
        if(siz[son[u]]<siz[v]) son[u]=v;
    }
    return ;
}

void dfs2(int u,int t)
{
    top[u]=t;
    if(!son[u]) return ;
    dfs2(son[u],t);
    for(int i=h[u];i;i=ne[i])
    {
        int v=e[i];
        if(v==fa[u] || v==son[u]) continue;
        dfs2(v,v);
    }
    return ;
}

int lca(int u,int v)
{
    while(top[u]!=top[v])
    {
        if(dep[top[u]]<dep[top[v]]) swap(u,v);
        u=fa[top[u]];
    }
    return dep[u]<dep[v] ? u : v;
}

// fa[rt]=0;    //注意初始化fa
dfs1(rt);
dfs2(rt,rt);

cin>>u>>v;
cout<<lca(u,v)<<endl;

8.2.dfs序+ST表在线求LCA\(O(N\log N+Q)-O(N\log N)\)

当u=v时,lca(u,v)是u。当u≠v时,lca(u,v)是dfs序上下标在\([dfn_u+1,dfn_v]\)中的深度最小的任意节点的父亲。

一种避免记录每个结点的父亲和深度的方法是直接在 ST 表的最底层记录父亲,比较条件是取时间戳较小的结点。

时间复杂度:\(O(N\log N+Q)\)。空间复杂度:\(O(N\log N)\)

int dfn[N],num;
int lg2[N],st[N][20];

void dfs(int u,int fa)
{
    dfn[u]=++num;
    st[dfn[u]][0]=fa;
    for(int i=h[u];i;i=ne[i])
    {
        int v=e[i];
        if(v==fa) continue;
        dfs(v,u);
    }
    return ;
}

void st_lca()
{
    for(int i=2;i<=n;i++) lg2[i]=lg2[i>>1]+1;
    for(int k=1;1+(1<<k)-1<=n;k++)
        for(int l=1;l+(1<<k)-1<=n;l++)
        {
            if(dfn[st[l][k-1]]<dfn[st[l+(1<<(k-1))][k-1]]) st[l][k]=st[l][k-1];
            else st[l][k]=st[l+(1<<(k-1))][k-1];
        }
    return ;
}

int lca(int u,int v)
{
    if(u==v) return u;
    if(dfn[u]>dfn[v]) swap(u,v);
    int k=lg2[dfn[v]-(dfn[u]+1)+1];
    if(dfn[st[dfn[u]+1][k]]<dfn[st[dfn[v]-(1<<k)+1][k]]) return st[dfn[u]+1][k];
    else return st[dfn[v]-(1<<k)+1][k];
}

dfs(rt,0);
st_lca();

cin>>u>>v;
cout<<lca(u,v)<<endl;

8.3.Tarjan算法离线求LCA\(O(\max\{N,Q\alpha(N+Q,N)\})-O(\max\{N,Q\})\)

本质上是并查集优化的向上标记法。

使用路径压缩/路径压缩和按秩合并优化的并查集:时间复杂度:\(O(\max\{N,Q\alpha(N+Q,N)\})\)。空间复杂度:\(O(\max\{N,Q\})\)

int n,m,root;
int h[N],e[M],ne[M],idx;
int p[N];   //并查集
bool vis[N];    //是否遍历完成即将回溯
int ans[N];
vector<PII> q[N];   //离线储存询问

void tarjan(int u,int fa)
{
    for(int i=h[u];i!=0;i=ne[i])
    {
        int v=e[i];
        if(v==fa) continue;
        tarjan(v,u);
        p[v]=u;
    }
    vis[u]=true;    //注意这行代码要在下行的上方,否则将不会处理询问LCA(u,u)
    for(auto it : q[u]) if(vis[it.x]) ans[it.y]=find(it.x);
    return ;
}

scanf("%d%d%d",&n,&m,&root);
for(int i=1;i<n;i++)
{
    int u,v;
    scanf("%d%d",&u,&v);
    add(u,v),add(v,u);
}
for(int i=1;i<=m;i++)
{
    int x,y;
    scanf("%d%d",&x,&y);
    q[x].push_back({y,i}),q[y].push_back({x,i});
}
for(int i=1;i<=n;i++) p[i]=i;
tarjan(root,-1);
for(int i=1;i<=m;i++) printf("%d\n",ans[i]);

8.4.其他方法求LCA

  • 树上倍增法在线求LCA\(O(N\log N+Q\log N)-O(N\log N)\)

    优点:可以与其他倍增数组结合,解决树上倍增问题。前置知识简单。

    时间复杂度:\(O(N\log N+Q\log N)\)。空间复杂度:\(O(N\log N)\)

int n,m,root;
int h[N],e[M],ne[M],idx;
int depth[N],fa[N][20];

void init_lca(int u,int father)
{
    for(int i=h[u];i!=0;i=ne[i])
    {
        int v=e[i];
        if(v==father) continue;
        depth[v]=depth[u]+1;
        fa[v][0]=u;
        for(int k=1;k<=17;k++)
            fa[v][k]=fa[fa[v][k-1]][k-1];
        init_lca(v,u);
    }
    return ;
}

int lca(int a,int b){
    if(depth[a]<depth[b]) swap(a,b);
    for(int i=17;i>=0;i--)
        if(depth[fa[a][i]]>=depth[b])
            a=fa[a][i];
    if(a==b) return a;  //这里不要忘记!!!
    for(int i=17;i>=0;i--)
        if(fa[a][i]!=fa[b][i]){
            a=fa[a][i];
            b=fa[b][i];
        }
    return fa[a][0];
}

depth[root]=1;//这里不要忘记!!!
init_lca(root,-1);

int x,y;
cin>>x>>y;
int p=lca(x,y);//此时p就是x和y的最近公共祖先
  • 欧拉序+ST表在线求LCA\(O(N\log N+Q)-O(N\log N)\)

    优点:可以与其他欧拉序数组结合,解决树上欧拉序问题。

    使用欧拉序把树上问题转化为线性问题,两个点的最近公共祖先就是两个点的欧拉序区间深度最小的节点,将问题转化为RMQ问题。

    注意欧拉序列的长度是2n-1。

    时间复杂度:\(O(N\log N+Q)\)。空间复杂度:\(O(N\log N)\)

int euler[N],seq[N*2],sidx;
int dep[N],lg2[N*2],st[N*2][20];

void dfs(int u,int fa)
{
    ++sidx;
    seq[sidx]=u,euler[u]=sidx;
    for(int i=h[u];i!=0;i=ne[i])
    {
        int v=e[i];
        if(v==fa) continue;
        dep[v]=dep[u]+1;
        dfs(v,u);
        seq[++sidx]=u;
    }
    return ;
}

void st_lca()
{
    for(int i=2;i<=2*n-1;i++) lg2[i]=lg2[i>>1]+1;
    for(int i=1;i<=2*n-1;i++) st[i][0]=seq[i];
    for(int k=1;1+(1<<k)-1<=2*n-1;k++)
        for(int l=1;l+(1<<k)-1<=2*n-1;l++)
        {
            if(dep[st[l][k-1]]<=dep[st[l+(1<<(k-1))][k-1]]) st[l][k]=st[l][k-1];
            else st[l][k]=st[l+(1<<(k-1))][k-1];
        }
    return ;
}

int lca(int x,int y)
{
    if(euler[x]>euler[y]) swap(x,y);
    int k=lg2[euler[y]-euler[x]+1];
    if(dep[st[euler[x]][k]]<=dep[st[euler[y]-(1<<k)+1][k]]) return st[euler[x]][k];
    else return st[euler[y]-(1<<k)+1][k];
}

dfs(s,-1);
st_lca();

cin>>u>>v;
cout<<lca(u,v)<<endl;

8.5.拓展应用

  1. 点集LCA

    给定一棵 \(n\) 个点的以点 \(rt\) 为根的树,请回答下面 \(q\) 次询问:给定点集中的点的 \(\text{LCA}\) 是谁?

    以点 \(rt\) 为根预处理 \(\text{dfs}\) 序。

    “给定点集中的点的 \(\text{LCA}\)”是“给定点集中的 \(\text{dfs}\) 序最小的和最大的两个点的 \(\text{LCA}\)”。

  2. 换根LCA

    给定一棵 \(n\) 个点的无根树,请回答下面 \(q\) 次询问:当以点 \(rt_i\) 为根时,点 \(u_i,v_i\)\(\text{LCA}\) 是谁?

    选定一个点为根(下文选定点 \(1\) 为根)预处理 \(\text{LCA}\)

    “当以点 \(rt_i\) 为根时,点 \(u_i,v_i\)\(\text{LCA}\)”是“当以点 \(1\) 为根时,在点 \(rt_i,u_i\)\(\text{LCA}\)、点 \(rt_i,v_i\)\(\text{LCA}\) 和点 \(u_i,v_i\)\(\text{LCA}\) 中深度最大的点”。

  3. 换根区间LCA

    给定一棵 \(n\) 个点的无根树,请回答下面 \(q\) 次询问:当以点 \(rt_i\) 为根时,编号在 \([l_i,r_i]\) 中的点的 \(\text{LCA}\) 是谁?

    选定一个点为根(下文选定点 \(1\) 为根)预处理 \(\text{dfs}\) 序和 \(\text{LCA}\)

    在以 \(\text{dfs}\) 序为下标的点的编号的序列上,在以 \(rt_i\) 为根的子树对应的区间和它的两个补区间(即在该序列上,下标为 \([1,\text{dfs}_{rt_i}-1],[\text{dfs}_{rt_i},\text{dfs}_{rt_i}+\text{size}_{rt_i}-1],[\text{dfs}_{rt_i}+\text{size}_{rt_i},n]\) 的三个区间,其中 \(\text{size}_{rt_i}\) 表示以 \(rt_i\) 为根的子树的点数)中各自找到最靠近区间左端点和右端点的编号在 \([l_i,r_i]\) 中的点,下文把这六个点称为关键点。

    “当以点 \(rt_i\) 为根时,编号在 \([l_i,r_i]\) 中的点的 \(\text{LCA}\)”是“当以点 \(rt_i\) 为根时,这六个关键点的 \(\text{LCA}\)”。

  4. LCA点集

    给定一个点集V,求满足\(\forall u,v\in V,\text{LCA}(u,v)\in V'\)的点集V'。

    按照时间戳将V中的点排序,排序后将相邻两个节点(包括首尾)的lca加入V',最后对V'去重。

    性质:

    1. \(\forall u,v\in V\cup V',\text{LCA}(u,v)\in V'\)
    2. V'的大小是\(O(|V|)\)
    3. 《8.5.拓展应用5.》。
  5. 求树上包含给定点集V的连通块的边集的总长度最小值

    按照时间戳将V中的点排序,排序后累加相邻两个节点(包括首尾)之间的路径长度为res,答案=res/2。

  6. 树上倍增法

    例题:次小生成树

  7. 求三个点的“集合点”,要求从三个点走到“集合点”的路径不重合:“集合点”是三点的两两间深度最大的lca处。

  8. 公式:对于已按入栈序排列的若干个点\(a_1,a_2,\cdots,a_n\)在树上构成的最小连通块,这些点在树上构成的最小连通块大小\(=\sum\limits_{i=1}^{n}dep[a_i]-\sum\limits_{i=2}^{n}dep[\text{LCA}(a_{i-1},a_{i})]-dep[\text{LCA}(a_1,a_2,\cdots,a_n)]+1\)

    Bonus:如何用线段树维护?在入栈序上建立线段树,线段树上第i个叶子节点代表入栈序中第i个原图节点的信息。上面式子的第一部分很好维护。对于第二、三部分,线段树新建mi、ma信息储存当前区间入栈序最小、大的原图节点编号,在pushup中,\(tr[u].w_2=tr[tr[u].lson].w_2+dep[lca(tr[tr[u].lson].ma,tr[tr[u].rson].mi)]+tr[tr[u].rson].w_2,tr[u].w_3=dep[lca(tr[u].mi,tr[u].ma)]\)。把关键点依次插入即可。

posted @ 2025-07-01 18:24  Brilliance_Z  阅读(48)  评论(0)    收藏  举报