代码编织梦想

LCA

对于一棵树 (可以是多叉树), 根为R

祖先
对于任意一个点x, 该点到R简单路径 (也就是从x一直往上走到R的路径)上的所有点
均是x祖先. (x也是自己的祖先!)

x上面的 (即深度小于x的), 不一定就是x的祖先. 还必须在xR简单路径

Depth[ R] = 0, 则x的祖先个数为: Depth[ x] + 1


简单路径 (正式定义是: 路径上没有重复点)
在树上的简单路径, 比如对于ab两点间的简单路径

  • ab简单路径一定是惟一的, 这由于树的特殊结构决定
  • x = depth[ a], y = depth[ b], 则简单路径[a, ..., b]其对应的depth3种可能情况
    1, x, x+1, x+2, ..., y-1, y, 表示, a点一直往下走到b, 路径长度为y - x
    2, x, x-1, x-2, ..., y-1, y, 表示a点一直往上走到b, 路径长度为x - y
    3, x, x+1, x+2, ..., h+1, h, h-1, ..., y-1, y 表示, a先一直往上走到一个点c, 然后从c再一直往下走到b
    路径长度为(x-h) + (y-h)
    不存在: (先往下走, 再往上走)的情况!
    c点, 就是ab的 (LCA), 下面会讲到.

公共祖先
S点集为: 既是a的祖先, 也是b的祖先
则, S点集为 ab的公共祖先.

S点集中, 所有点的深度均不同. 其实这个点集, 就是 (aR简单路径) 与 (bR简单路径) 的 交集.


最近公共祖先 LCA: least common ancestor
ab的LCA为: S点集(即公共祖先中), 深度最大的点, 他是唯一的

算法

倍增

LCA( a, b), 假设depth[ a] >= depth[ b]

  • 第一步, 将a往上走, 移动到和b同一层(同一深度) 点, 记为aa
    对于d = depth[ a] - depth[ b], 要走b步, 根据倍增原理, b = [0, 1, ..., N], 平均下来, 最少是O( logN)次. 即, 二进制拆分.
    因此, 我们需要一个Fa[ x][ k]数组, 表示: x点, 往上走 2 k 2^k 2k步后的祖先
    预处理这个数组是N * logN, 即dfs整个树
  • 第二步, 此时aa 与 b是在同一层, 让他俩同时往上走, 走到LCA的儿子
    为什么不走到LCA呢? 而是走到他的儿子呢?
    假如Fa[ aa][ 0, 1, 2, ...]为: a0, a1, a2, a3, ...
    … … Fa[ b][ 0, 1, 2, ...]为: b0, b1, b2, b3, ...
    我们看他的bool( ai == bi)这个布尔数组, 记作c: c0, c1, c2, c3, ...
    假如, a2是LCA, 即: a2 == b2 == LCA, 则布尔数组为: 0, 0, 1, 1, 1, 1, ... 则, LCA为: 布尔数组中 最左侧的1
    但是, LCA可能是 介于a1 和 a2之间的, 此时其布尔数组为: 0, 0, 1, 1, 1...
    选择最左侧的1,即a2 == b2, 他确实是公共祖先, 但他不是最近公共祖先!!
    如果是 二分 , 你跳到a2, 还能往回跳 (即往下跳), 但这里是 倍增 , 只能单向往前走, 不能往回走!
    因此, 如果ci为真 (aa 与 b的公共祖先),就跳过去,这是不对的
    应该是: 跳到 最左侧的1的左侧0位置 , 即: aa 到 a1点, b 到 b1点 (当然a1 != b1)
    这样, 再以a1 b1继续跳, 仍然是跳到 最左侧的1的左侧0位置 , 最终, a b会跳到: LCA的两个儿子上 (LCA有>=2个儿子, 多叉树, 但a b在其中的两个上)

算法注意点

  • 对于数, 我们知道, 树根Root是可以任意的! 即选择[1, ..., N]哪个点当做树根, 其仍然是一个树!
    这可以想象成: 只有树根有一个钉子, 定在那. 其他所有点都下垂
    但是, 不同的树根, ab两点的LCA可能是不同的!
    比如, 选择a作为树根, 自然LCA( a, b) = a; 选择b作为树根, 自然LCA( a, b) = b
    因此, 只有树根确定了, LCA才能确定.
    … 但是, 涉及到LCA的问题, 不一定题目会给出固定的树根! 比如, 树上前缀和/差分, 都会涉及到LCA
    … 但是, 树根选择哪个都是正确的, 都可以正确的求出前缀和/差分, 只要找到LCA即可, 不管是在哪个树根下.

  • Depth[ Root] = 0, 这样做的好处是: 对于一点x, x 与 Root的简单路径上 正好有 Depth[ x]条边.
    也就是, x点, 往上跳Depth[ x]步, 就是Root

  • 设置一个int Log[ N]数组, Log[ a] = b满足: 2^b <= a, 且b最大
    这样, 每次求lca( a, b)时, 往上跳时, 不需要从 Log( N) 从而产生一堆非法值
    而直接从Log( Depth[ a])开始.
    … 而且, 还有个好处是: 当constexpr int N_ = xx;点数改变了, 只需修改下: Fa[ N_ ][ 21]中的21这个数值
    … 其他代码不用改, 因为代码里用的都是Log, 而Log会根据N_的大小来进行循环
    Log的大小是: int Log[ N_];, 也不用动.

  • Log[ 0]是未定义的, Fa[ Root][ >= 0]都是未定义的, 也都是非法值.
    要避开这些.

代码

题目链接

LITERAL_ int N_ = int( 5e5) + 5;
int N, M, Root; //< (点数) (查询数) (根)
int Head[ N_], Ver[ N_ * 2], Nex[ N_ * 2], Edges;
int Log[ N_]; //< Log[ a] = b;  2^b <= a, 最大的b
int Depth[ N_], Fa[ N_][ 21];
//--
void Build_graph(){
    Edges = 0;
    memset( Head, -1, sizeof( Head));
}
void Add_edge( PAR_( int, _a), PAR_( int , _b)){
    Ver[ Edges] = _b;
    Nex[ Edges] = Head[ _a];
    Head[ _a] = Edges;
    ++ Edges;
}
void Dfs_lca( PAR_( int, _cur), PAR_( int, _fa)){
    for( int nex, i = Head[ _cur]; ~i; i = Nex[ i]){
        nex = Ver[ i];
        if( nex != _fa){
            Depth[ nex] = Depth[ _cur] + 1;
            //--
            Fa[ nex][ 0] = _cur;
            FOR_( step, 1, Log[ Depth[ nex]], 1){
                Fa[ nex][ step] = Fa[ Fa[ nex][ step - 1]][ step - 1];
//                DEBUG_( nex, step, Fa[ nex][ step]); //< 在`Build_lca()`完后, 最好调试测试下
            }
            //--
            Dfs_lca( nex, _cur);
        }
    }
}
void Build_lca(){
    { //* build `Log`
        FOR_( i, 1, N, 1){
            if( 1 == i){
                Log[ 1] = 0;
            }
            else{
                if( ( 1 << ( Log[ i - 1] + 1)) == i){
                    Log[ i] = Log[ i - 1] + 1;
                }
                else{
                    Log[ i] = Log[ i - 1];
                }
            }
        }
    }
    //--
    Depth[ Root] = 0;
    Dfs_lca( Root, -1);
}
int Lca( PAR_( int, _a), PAR_( int, _b)){
    int low = _a, hig = _b;
    if( Depth[ low] < Depth[ hig]){
        swap( low, hig);
    }
    while( Depth[ low] != Depth[ hig]){
        low = Fa[ low][ Log[ Depth[ low] - Depth[ hig]]];
    }
    if( low == hig){ //< 如果不判断, 假如low=Root, 则下面代码中的Log[ 0]是个未定义行为
        return low;
    }
    FOR_RE_( i, Log[ Depth[ low]], 0, 1){ //< 不是 `Log[ low]`!
        if( Fa[ low][ i] != Fa[ hig][ i]){
            low = Fa[ low][ i];
            hig = Fa[ hig][ i];
        }
    }
    assert( Fa[ low][ 0] == Fa[ hig][ 0]);
    return Fa[ low][ 0];
}
void __Solve(){
    Build_graph();
    scanf("%d%d%d", &N, &M, &Root);
    FOR_( i, 1, N-1, 1){
        int a, b;
        scanf("%d%d", &a, &b);
        Add_edge( a, b);
        Add_edge( b, a);
    }
    Build_lca();
    FOR_( i, 1, M, 1){
        int a, b;
        scanf("%d%d", &a, &b);
        printf("%d\n", Lca( a, b));
    }
}
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_42712593/article/details/126011847

tarjan离线算法求最近公共祖先(lca)-爱代码爱编程

LCA问题,是当给定一个有根树T时,对于任意两个结点u、v,要求LCA(T,u,v),即是要找得一个离根最远的结点x,使得x同时是u和v的祖先。也就是距离它们最近的祖先。 结点3和结点4的最低公共祖先是结点2,即LCA(3 4)=2。结点3和结点2的最低公共祖先为2,即 LCA(3 2)=2。同理:LCA(5 6)=4,LCA(6 10)=1

tarjan离线算法 (lca最近公共祖先)_bestsort的博客-爱代码爱编程

Tarjan离线算法是利用并查集和DFS来达到离线处理的目的 我们都知道,对于一棵树,后序遍历一遍,访问它的根的时机一定是后与它的孩子的。换一句话,当开始遍历它的根节点的时候,它遍历过的孩子的公共祖先一定是这个根而这也就成

【C++】最近公共祖先 LCA-爱代码爱编程

最近公共祖先 百科名片简单引入LCA的算法暴力枚举法Tarjan离线算法倍增算法例题:题目描述输入描述输出描述样例输入样例输出代码 百科名片 最近公共祖先Lowest Common AncestorsLCA简单引入 对于有根树T的两个结点u、v,最近公共祖先LCA(T,u,v)表示一个结点x,满足x是u、v的祖先且x的深度尽可能大。 红色的

最近公共祖先LCA-爱代码爱编程

最近公共祖先(Lowest Common Ancestors,LCA)指有根树中距离两个节点最近的公共祖先。祖先指从当前节点到树根路径上的所有节点。 u和v的公共祖先指一个节点既是u的祖先,又是v的祖先。u和v的最近公共祖先指距离u和v最近的公共祖先。若v是u的祖先,则u和v的最近公共祖先是v。      可以使用LCA求解树上任意两点之间的

使数组变成 <连续递增>的 最小修改次数-爱代码爱编程

添加链接描述 给定n=1e5的数组,每个元素是[-1e9, 1e9]。 一次操作为: 将一个元素的值 修改为 任意值。 问最少几次操作,可以使得 数组是《连续递增》 所谓《连续递增》是: 将该数组sort后,数组是一个 公差为1 的等差数组。 比如: [6, 5, 2, 3, 4] 这就是《连续递增》的。因为他sort后是:[2, 3, 4, 5,

最近公共祖先LCA倍增算法-爱代码爱编程

LCA定义 LCA(Least Common Ancestors):最近公共祖先即两个结点最近的公共祖先 由上图可以看到 5号结点和7号结点的LCA为3号结点,9号结点和7号结点的LCA为7号结点。 一般算法 首先可以将两个结点统一到相同深度,然后一起向上一步一步走,直到他们踩到相同点,则该点为他们的LCA 。 (

【树上算法】最近公共祖先(LCA)-爱代码爱编程

最近公共祖先 前言 最近公共祖先是在树上的一个算法。这里就提到了树。树想必大家都再熟悉不过了,在计算机中,我们可以把树看成一种特殊的图。 这种图就叫有向无环联通图。顾名思义,没有环,有向图, n n

最近公共祖先LCA(倍增法和Tarjan算法)(例题)-爱代码爱编程

求LCA通常用两种方法: 1. 倍增法(最常用的在线处理方法): 用一个数组 f [ i ]

[算法]最近公共祖先(LCA)-爱代码爱编程

最近公共祖先(LCA) #include<cstdio> #include<cstring> #include<iostream> #include<algorithm> #include<queue> using namespace std; const int maxn = 5e5 + 1

倍增算法之最近公共祖先 (LCA)-爱代码爱编程

  这里对一些具体的细节不在过多解释,代码中的注释已经比较详细了,这里我们重点解释一下对f数组进行更新时的计算公式是什么意思,f[now][i]=f[f[now][i-1]][i-1],f[now][i]指的是结点now的第2^j级祖先的编号,我们可以将具体的数值带入进行解释,比如将i=1带入,f[now][1]=f[f[now][0]][0]就表示

最近公共祖先 LCA-爱代码爱编程

概念 子节点(儿子):一个节点含有的子树的根节点称为该节点的子节点; 父节点(父亲):若一个节点含有子节点,则这个节点称为其子节点的父节点; 根节点:没有父节点的节点; 兄弟节点:具有相同父节点的节点互称为兄弟节点; 祖先:父亲以及父亲的祖先; 后代:儿子以及儿子的后代; 子树:子节点及其后代构成的树 节点的度:一个节点相连的点的个数称为该

最近公共祖先(LCA)-爱代码爱编程

最近公共祖先 向上标记法树上倍增Tarjan发明的算法转化为求区间最小值问题 最近公共祖先问题是树中的常考问题,比如我们有一棵树(可以是多叉的),两个点的最近公共祖先就是字面的意思,比如说6、5节点的最近公共祖先就是2,而4和7的最近公共祖先我们规定为4. 最近公共祖先问题就是给我们两个点让我们求他们的最近公共祖先的问题。 下面我就介绍一下我

【图论】—— 最近公共祖先(lca)_玄澈_的博客-爱代码爱编程

给定一颗树,若节点 z 既是 节点 x 的祖先, 也是节点 y 的祖先,那么称 z 为 x、y 的公共祖先。 在 x、y 的所有公共祖先中,深度最大的一个称为 x、y 的最近公共祖先, 也称 LCA(x, y) LCA(x, y) 是 x 到根的路径与 y 到根的路径的交汇点。它也是 x 与 y 之间的路径上深度最小的节点。 树上倍增法