dfs序求lca的讲解、相关常见错误及相关问题

本文暂时弃坑,以后会重构。

本文主要是用于警示自己避免犯错。

相关博客

最近公共祖先 | st 表求 lca 别用欧拉序了!!!
冷门科技 —— DFS 序求 LCA

算法讲解

这是以节点 \(1\) 为根节点的一棵树
pPfe3jI.png
我们在根节点进行 dfs,在 dfs 的开始将到达的节点加入一个序列的末尾中,就得到了所有节点拼成的一个序列,这个序列就是 dfs 序,节点在 dfs 序中的位置被称为节点的 \(dfn\)

对这颗树进行 dfs,可求得它的 dfs 序为 \(1\),\(2\),\(4\),\(5\),\(3\),\(6\)(dfs 序不唯一,也可能为\(1\),\(2\),\(5\),\(4\),\(3\),\(6\))。节点 \(1\)\(6\)\(dfn\) 值分别为 \(1\),\(2\),\(5\),\(3\),\(4\),\(6\)
得到这个序列有啥用呢?如何求两个节点的 lca?
我们可以观察到某节点的祖先在 dfs 序中一定在该节点的前面,以某节点为根节点的子树的所有子节点(即除根节点外的所有节点)都在根节点的后面。所以不能直接在节点 \(u\) 和节点 \(v\) 之间找一个节点作为它们的 lca。

我们来分类讨论,从一个简单的情况切入这个问题。
(下面的讨论默认 \(dfn_u\)<=\(dfn_v\)

  1. \(u\)\(v\) 的祖先。

还是以上面那个图为例。
我们要找 \(1\)\(4\) 的 lca,显然 lca 为 \(1\)
观察从 \(1\)\(4\) 的 dfs 序,可以发现整个序列都是在 根节点为 \(1\) 的子树上进行的,一定经过 \(1\) 的子节点才到达 \(4\)
难道是直接返回从 \(1\)\(4\) 中深度最小的节点吗?
由前面的性质可知某节点的祖先在 dfs 序中一定在该节点的前面,万一 \(u\) 不是 \(v\) 的祖先,这个方法就完全错了。
我们再回去观察一下。
\(1\)\(4\) 经过了 \(1\) 的孩子,也经过了 \(4\) 的祖先,\(1\)\(4\) 的 lca 一定是 \(4\) 的祖先,那我们找到 \(4\) 的祖先中除去 \(1\) 深度最小的那一个的父节点是不是就是 lca?

参考代码

dfs 序求 lca 的参考代码如下。

#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e5+10,MAXLOG2N=20;
int N,M,S,cnt,head[MAXN],dfn[MAXN],dfncnt,stlca[MAXLOG2N][MAXN],LOG2[MAXN],deep[MAXN],anc[MAXN];
struct EDGE
{
    int v,nxt;
}edge[MAXN<<1];
void add(int u,int v)
{
    ++cnt;
    edge[cnt].v=v;
    edge[cnt].nxt=head[u];
    head[u]=cnt;
}
void dfs(int u)
{
    stlca[0][dfn[u]=++dfncnt]=u;
    for(int i=head[u];i;i=edge[i].nxt)
    {
        int v=edge[i].v;
        if(v!=anc[u]){anc[v]=u;deep[v]=deep[u]+1;dfs(v);}
    }
}
int deepget(int u,int v)//得到深度浅的节点
{
    return deep[u]<deep[v]?u:v;
}
void stlca_init()
{
    //求解log2部分
    for(int i=2;i<=N;++i){LOG2[i]=LOG2[i>>1]+1;}
    //建ST表部分
    for(int j=1;(1<<j)<=N;++j)
    {
        int temp=N-(1<<j)+1;
        for(int i=1;i<=temp;++i)
        {
            stlca[j][i]=deepget(stlca[j-1][i],stlca[j-1][i+(1<<(j-1))]);
        }
    }
}
int lca(int u,int v)
{
    if(u==v)return u;
    if(dfn[u]>dfn[v])swap(u,v);//保持dfn[u]<dfn[v]
    int k=LOG2[dfn[v]-dfn[u]];
    int lcason=deepget(stlca[k][dfn[u]+1],stlca[k][dfn[v]-(1<<k)+1]);
    return anc[lcason];
}
int main()
{
    ios::sync_with_stdio(0);cin.tie(0);
    cin>>N>>M>>S;
    for(int i=1;i<N;++i)
    {
        int x,y;cin>>x>>y;
        add(x,y);
        add(y,x);
    }
    deep[S]=1;//根节点深度为1好看
    dfs(S);
    stlca_init();
    while(M--)
    {
        int a,b;cin>>a>>b;
        cout<<lca(a,b)<<'\n';
    }
    return 0;
}

常见错误

  1. 无向边存储时少一次变成有向边。

太()()了。

  1. dfs 过程中忘记添加祖先及深度的记录。
void dfs(int u)
{
    stlca[0][dfn[u]=++dfncnt]=u;
    for(int i=head[u];i;i=edge[i].nxt)
    {
        int v=edge[i].v;
        if(v!=anc[u])dfs(v);
    }
}

这都能写错???

  1. 查找深度小的节点的函数写错。
int deepget(int u,int v)//得到深度浅的节点
{
    return dfn[u]<dfn[v]?u:v;
}

我晕。

  1. ST 表建表写错
for(int j=1;(1<<j)<=N;++j)
{
    for(int i=1;i+(1<<j)-1<=N;++i)
    {
        stlca[j][i]=deepget(stlca[j-1][i],stlca[j-1][i+(1<<j/*应为1<<(j-1)!*/)]);
    }
}

我怎么会在前面的 log 下标填 j-1 后填了个 i+(1<<j)

  1. lca 求解过程中忘了写 u==v 的情况。
  2. lca 求解过程中忘了将 \(dfn\) 较大的 \(u\)\(v\) 交换。

这两种情况倒是没写错过,不过以防万一还是加上。

  1. 直接把 lcason 返回。

st 表求的是 dfs 序中 \(u\) 所在位置加上 \(1\)\(v\) 的位置中的深度最小的节点,并不是 lca,而是 lca 的孩子。

  1. 左移与加法运算的时候不加括号。
stlca[j][i]=deepget(stlca[j-1][i],stlca[j-1][i+1<<(j-1)]);

左移的优先级小于加法,不加括号导致运算的顺序错误,建议多加括号保险

一些问题

  1. 我的 \(k\) 在求解过程中写错了,原本应该是下面的代码的前者(dfn[u]+1dfn[v] 这个序列的长度),却写成了后者(序列长度减去 \(1\)),但是没有被 hack 掉,是怎么回事?
int k=LOG2[dfn[v]-dfn[u]];//前者
int k=LOG2[dfn[v]-(dfn[u]+1)];//后者

可以看出这两种写法有区别当且仅当 dfn[v]-dfn[u]\(2\) 的倍数。
此时两种写法虽然 log 下标不同,但可以看出其覆盖范围是相同的。
pPfEpAe.png
看的出来,后者写法的两段区间刚好无缝衔接(衔接的两个端点差为 \(1\)),所以两种写法的输出都是正确的。

后言

写的可能不是很完整,以后可能补充。

posted @ 2023-09-16 09:07  LiJoQiao  阅读(83)  评论(0编辑  收藏  举报