LCA 问题的不同解法

0 序

为了强化笔者对LCA的理解,故作此文。

1 DFS序求LCA

1.1 算法介绍

考虑树上的两个节点 $u$, $v$ 和其祖先 $d$,我们之所以使用欧拉序求解 LCA 是因为在欧拉序中 $d$ 一定在 $u,v$ 之间出现。但对于 DFS 序来说,$d$ 一定在 $u,v$ 之前出现。

令 $u$ 的 DFN 小于 $v$,且 $u\ne v$ 。

当 $u,v$ 没有祖先关系时。在 DFN 中最先出现的是 $d$ ,其次是 $u$,最后是 $v$ 。设 $v'$ 是 $d$ 的儿子。根据 DFS 的特性可知,$v'$ 在 $u\sim v$ 的 DFN 序之间。

这意味着我们只需要求出 $u \sim v$ 的 DFS 序之间深度最小的一个节点,那么他的父亲便是我们苦苦寻找的 LCA。

如何保证这个方法的正确性?在 $u\sim v$ 的 DFS 序之间,$d$ 一定不会存在,而 $d$ 的儿子一定存在。

当 $u, v$ 存在祖先关系,这更容易判断了,根据定义, $u$ 一定是 $v$ 的祖先。查询的序列由 $[dfn_u,dfn_v]$ 变成 $[dfn_u+1,dfn_v]$ 。为什么要 "$+1$"?根据上一种情况可知,此时的祖先是 $u$ ,但此时深度最小的节点一定是 $u$,如果在这时取它的父亲,很明显不是我们要找的正确答案。

很显然,当$u,v$ 没有祖先关系时, $u$ 一定不等于 $d'$ ,所以刚刚提出的结论适用 $u$ 的 DFN 小于 $v$,且 $u\ne v$ 所有的情况。

若 $u=v$ 时,它们的 LCA 一定是 $u$ ,这是唯一一种需要特判的情况。

综上所述,若 $u\ne v$ ,$u,v$ 的 LCA 是 $[dfn_u,dfn_v]$ 中深度最小的点的父亲,若 $u=v$ 时,$u,v$ 的 LCA 是 $u$。

1.2算法实现

可以通过 ST 表来查询 $[dfn_u,dfn_v]$ 中深度最小的点。时间复杂度 $O(nlogn)$ 。

以下是模板题目 P3379 的代码:

// P3379 【模板】最近公共祖先(LCA)
// https://www.luogu.com.cn/problem/P3379
// 2023-11-09 21:40
#include <iostream>
#include <vector>
#include <cmath>
using namespace std;
const int maxn = 500000 + 5;

int n, m, s;
vector<int> G[maxn];
int dfn[maxn], mi[20][maxn];

int get(int x, int y) {
    return dfn[x] < dfn[y] ? x : y;
}

void dfs(int x, int f) {
    dfn[x] = ++dfn[0]; mi[0][dfn[x]] = f;
    for(auto to : G[x]) if(to != f) dfs(to, x);
}

int lca(int x, int y) {
    if(x == y) return x;
    if((x = dfn[x]) > (y = dfn[y])) swap(x, y);
    int d = log2(y - x++);
    return get(mi[d][x], mi[d][y - (1 << d) + 1]);
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    // freopen("AKIOI.in", "r", stdin);
    // freopen("AKIOI.out", "w", stdout);
    cin >> n >> m >> s;
    for(int i = 1, u, v; i < n; i++)  {
        cin >> u >> v;
        G[u].push_back(v); G[v].push_back(u);
    }
    dfs(s, 0);
    for(int i = 1; i <= log2(n); i++) for(int j = 1; j + (1 << i) - 1 <= n; j++) 
        mi[i][j] = get(mi[i - 1][j], mi[i - 1][j + (1 << i - 1)]);
    while(m--) {
        int x, y;
        cin >> x >> y;
        cout << lca(x, y) << "\n";
    }
    return 0;
}

/*
编程语言
C++14 (GCC 9)
代码长度
1.25KB
用时
1.64s
内存
77.61MB
GGapa

所属题目
P3379 【模板】最近公共祖先(LCA)
评测状态
Accepted
评测分数
100
提交时间
2023-11-09 22:02:41
*/

2 树链剖分 LCA

2.1 算法介绍

对于一个父节点的所有儿子来说,在它们之间子树大小最大的便是重儿子($hson$),其余的儿子是轻儿子。我们可以对一棵树上所有的非叶子节点进行操作,求出他们的重儿子。从一个节点 $u$ 出发,每次递归到下一个重儿子直到叶子节点未知,可得一个递归函数 $f(x)=f(hson_x)$ 。在递归的过程中访问到的节点可以连成一条链,这条链便是重链。每条重链的出发点被称为 $top$ 。

考虑树上的两个节点 $u$, $v$ 和其祖先 $d$,

  1. 若 $top_u = top_v$ 这说明 $u,v$ 在一条重链上,$u,v$ 一定存在祖先关系,此时 $u,v$ 中深度较小的节点便是他们的 LCA。
  2. 否则,需要把深度较大的节点跳到这条重链的顶端。并重复执行操作1。

为什么不能同时将 $u, v$ 上跳?令 $u$ 的深度大于 $v$ 因为有时深度较大的节点的 $top$ 便是 $v$ 所在的重链,每跳一次都会使它离开当前的重链。这时 $v$ 往上跳就不能达成条件,效率低下。

时间复杂度 $O(n\log n)$

2.2 算法实现

需要进行两次递归,第一次递归求出每个非叶子节点的重儿子;第二次循环求出 $top$,也就是重链。

以下是模板题目 P3379 的代码:

// P3379 【模板】最近公共祖先(LCA)
// https://www.luogu.com.cn/problem/P3379
// 2023-11-09 20:41
#include <iostream>
#include <vector>
using namespace std;
const int maxn = 500000 + 5;

int n, m, s; 
vector<int> G[maxn];
int si[maxn], top[maxn], hson[maxn], fa[maxn], dep[maxn];

void dfs1(int x, int f) {
    si[x] = 1; dep[x] = dep[f] + 1; fa[x] = f;
    for(auto to : G[x]) {
        if(to == f) continue;
        dfs1(to, x);
        si[x] += si[to];
        if(si[to] > si[hson[x]]) hson[x] = to;
    }
}

void dfs2(int x, int tp) {
    top[x] = tp;
    if(!hson[x]) return ;
    dfs2(hson[x], tp);
    for(auto to : G[x]) {
        if(to == fa[x] || to == hson[x]) continue;
         dfs2(to, to);       
    }
}

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

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    // freopen("AKIOI.in", "r", stdin);
    // freopen("AKIOI.out", "w", stdout);
    cin >> n >> m >> s;
    for(int i = 1, u, v; i < n; i++) {
        cin >> u >> v;
        G[u].push_back(v);
        G[v].push_back(u);
    }
    dfs1(s, 0);
    dfs2(s, s);
    while(m--) {
        int x, y;
        cin >> x >> y;
        cout << lca(x, y) << "\n";
    }
    return 0;
}

/*
编程语言
C++14 (GCC 9)
代码长度
1.37KB
用时
1.86s
内存
55.85MB
GGapa

所属题目
P3379 【模板】最近公共祖先(LCA)
评测状态
Accepted
评测分数
100
提交时间
2023-11-09 21:04:30
*/

3 倍增 LCA

3.1 算法介绍

相对于暴力算法,倍增 LCA 减少了向上跳的次数,如何减少?每次从大到小尝试跳 $2^i$ 个节点,直到到达目的地。

为了方便跳,需要记录每一个节点 $2^i$ 级的祖先。

考虑树上的两个节点 $u$, $v$,需要先使 $dep_u=dep_v$

接着,两个节点同时往上跳,直到$fa{vi}=fa{ui}$为止,此时 $fau$ 便是 $u,v$ 的 LCA。有一点需要注意,要跳到 $fa{ui}=fa_{vi}$ 而不是 $u=v$ 是为了防止跳过了,找到的是他们的公共祖先,而不是 LCA。

时间复杂度$O(n\log n)$

3.2 算法实现

以下是模板题目 P3379 的代码:

// P3379 【模板】最近公共祖先(LCA)
// https://www.luogu.com.cn/problem/P3379
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>
using namespace std;
const int maxn = 500000 + 5;
const int maxk = 21; // log2(maxn) + 1

int fa[maxk][maxn];
int depth[maxn];
vector<int> vec[maxn];

void dfs(int x, int f)
{
    fa[0][x] = f;
    depth[x] = depth[f] + 1;
    for (int i = 1; i < maxk; i++)
    {
        fa[i][x] = fa[i - 1][fa[i - 1][x]];
    }
    for (auto to : vec[x])
    {
        if (to == f)
            continue;
        dfs(to, x);
    }
}

int LCA(int x, int y)
{
    if (depth[x] < depth[y])
        swap(x, y);
    for (int i = maxk - 1; i >= 0; i--)
    {
        if (depth[fa[i][x]] >= depth[y])
            x = fa[i][x];
    }
    if (x == y)
        return x;
    for (int i = maxk - 1; i >= 0; i--)
    {
        if (fa[i][x] != fa[i][y])
        {
            x = fa[i][x];
            y = fa[i][y];
        }
    }
    return fa[0][x];
}

int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    int n, m, s;
    scanf("%d%d%d", &n, &m, &s);
    for (int i = 1, x, y; i < n; i++)
    {
        scanf("%d%d", &x, &y);
        vec[x].push_back(y);
        vec[y].push_back(x);
    }
    dfs(s, 0);
    for (int i = 0; i < m; i++)
    {
        int a, b;
        scanf("%d%d", &a, &b);
        cout << LCA(a, b) << endl;
    }
    return 0;
}
/*
编程语言
C++14 (GCC 9) O2
代码长度
1.43KB
用时
5.53s
内存
90.44MB
GGapa

所属题目
P3379 【模板】最近公共祖先(LCA)
评测状态
Accepted
评测分数
100
提交时间
2023-07-31 15:39:45
*/

倍增的常数很大,所以说花了接近 5s。

4 各种 LCA 算法的对比

  • 预处理时间复杂度均相同,常数倍增最大。

  • 单次查询复杂度:DFS 序 $<$ 树链剖分 $=$ 倍增

  • 单次查询常数大小:树链剖分 $<$ DFS 序 $<$ 倍增

  • 个人认为 DFS 序代码难易程度较低。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇