洛谷题单指南-图论之树-P3379 【模板】最近公共祖先(LCA)

原题链接:https://www.luogu.com.cn/problem/P3379

题意解读:最近公共祖先(Lowest Common AncestorLCA)是指在有根树中,两个节点的所有公共祖先中离根最远(即深度最大)的那个祖先节点。

解题思路:(以下题解部分内容为AI生成)

方法一:朴素算法

基本思想

朴素算法求解 LCA 的核心思路是利用树的层次结构特性,先将两个目标节点调整到同一深度,然后同时沿着它们的父节点指针向上移动,直到找到它们的公共祖先节点,这个公共祖先节点就是最近公共祖先。

具体步骤

  1. 深度优先搜索(DFS)预处理
    • 从树的根节点开始进行深度优先搜索。在搜索过程中,记录每个节点的父节点信息(存储在 fa 数组中)以及该节点的深度信息(存储在 depth 数组中)。深度信息用于后续调整节点到同一深度,父节点信息用于向上回溯查找公共祖先。
    • 对于每个节点,其深度等于其父节点的深度加 1。例如,若节点 u 的父节点是 p,则 depth[u] = depth[p] + 1。
  2. 调整节点深度
    • 比较两个目标节点 u 和 v 的深度。如果 u 的深度大于 v 的深度,那么让 u 沿着其父节点指针向上移动,直到 u 和 v 的深度相同。移动的次数等于它们深度的差值。
    • 例如,若 depth[u] = 5,depth[v] = 3,则 u 需要向上移动 5 - 3 = 2 次。
  3. 同时向上移动查找公共祖先
    • 当 u 和 v 处于同一深度后,同时沿着它们的父节点指针向上移动,每次移动一个节点。在移动过程中,不断比较 u 和 v 是否相等。
    • 如果相等,那么这个节点就是它们的最近公共祖先;如果不相等,则继续向上移动,直到找到相等的节点。

复杂度分析

  • 时间复杂度
    • 预处理阶段,深度优先搜索需要遍历树的所有节点,时间复杂度为O(n),其中n是树的节点数量。
    • 每次查询时,最坏情况下需要将节点向上移动到根节点,因此查询的时间复杂度也是O(n)。如果有q个查询,总的时间复杂度为O(n+qn)。
  • 空间复杂度:主要用于存储 parent 数组和 depth 数组,每个数组的长度为n,因此空间复杂度为O(n) 。

适用场景

该方法适用于树的节点数量较少,且查询次数不多的情况。由于其查询时间复杂度较高,在大规模数据和频繁查询的场景下效率较低。

90分代码:

#include <bits/stdc++.h>
using namespace std;

const int N = 500005;

vector<int> g[N];
int depth[N];
int fa[N];
int n, m, s;

void dfs(int u, int parent)
{
    fa[u] = parent;
    depth[u] = depth[parent] + 1;
    for(auto v : g[u])
    {
        if(v == parent) continue;
        dfs(v, u);
    }
}

int lca(int u, int v)
{
    if(depth[u] < depth[v]) swap(u, v);
    while(depth[u] > depth[v]) u = fa[u];
    while(u != v)
    {
        u = fa[u];
        v = fa[v];
    }
    return u;
}

int main()
{
    cin >> n >> m >> s;
    int u, v;
    for(int i = 1; i < n; i++)
    {
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }

    dfs(s, 0);
    while(m--)
    {
        cin >> u >> v;
        cout << lca(u, v) << endl;
    }

    return 0;
}

方法二:倍增算法(Binary Lifting)

基本思想

倍增算法的核心在于利用二进制的思想,通过预处理每个节点的2^i级祖先(i=0,1,2...),将查找最近公共祖先的过程进行优化。在查询时,利用这些预处理的信息可以快速地进行跳跃式的向上查找,避免了朴素算法中每次只能向上移动一个节点的低效方式。

具体步骤

  1. 深度优先搜索(DFS)预处理
    • 同样从树的根节点开始进行深度优先搜索,记录每个节点的父节点信息(存储在 parent[u][0] 中)和深度信息(存储在 depth 数组中)。这里的 parent[u][i] 表示节点 u 的2^i级祖先。
    • 例如,parent[u][0] 就是节点 u 的直接父节点。
  2. 预处理每个节点的2^i级祖先
    • 通过动态规划的思想,利用已经计算出的 parent[u][i - 1] 来计算 parent[u][i]。具体公式为 parent[u][i] = parent[parent[u][i - 1]][i - 1]。
    • 这是因为节点 u 的2^i级祖先可以通过先找到其2^(i-1)级祖先,再找到该祖先的2^(i-1)级祖先得到。例如,节点 u 的2^2=4级祖先可以通过先找到其2^1=2级祖先 v,再找到 v 的2^1=2级祖先得到。
  3. 调整节点深度
    • 比较两个目标节点 u 和 v 的深度。如果 u 的深度大于 v 的深度,利用预处理的2^i级祖先信息,通过二进制拆分的方式,让 u 快速向上移动到和 v 相同的深度。
    • 例如,若 depth[u] - depth[v] = 5,可以将 5 拆分为2^2+2^0,则 u 先向上移动到其2^2级祖先,再向上移动到该祖先的2^0级祖先,从而快速到达和 v 相同的深度。
  4. 同时向上移动查找公共祖先
    • 当 u 和 v 处于同一深度后,从最大的i(log2(n))开始,尝试让 u 和 v 同时向上移动2^i级祖先。如果移动后 u 和 v 不相等,则更新 u 和 v 为它们移动后的节点;如果相等,则不移动,继续尝试更小的 。
    • 最后,当i = 0时,u 和 v 的父节点就是它们的最近公共祖先。

复杂度分析

  • 时间复杂度
    • 预处理阶段,需要遍历树的所有节点,并且对于每个节点要计算其O(logn)个2^i级祖先,因此预处理的时间复杂度为O(nlogn)。
    • 每次查询时,最多需要进行O(logn)次跳跃操作,因此查询的时间复杂度为O(logn)。如果有q个查询,总的时间复杂度为O(nlogn+qlogn)。
  • 空间复杂度:主要用于存储 parent 数组,其大小为n*logn,因此空间复杂度为O(nlogn)。

适用场景

该方法适用于树的节点数量较多,且查询次数频繁的情况。由于其查询时间复杂度为O(logn),在大规模数据和频繁查询的场景下具有较好的性能。

100分代码:

#include <bits/stdc++.h>
using namespace std;

const int N = 500005;

vector<int> g[N];
int depth[N];
int fa[N][20]; //fa[i][j]表示从节点i往上2^j个父节点的编号
int n, m, s;

void dfs(int u, int parent)
{
    fa[u][0] = parent;
    depth[u] = depth[parent] + 1;
    for(auto v : g[u])
    {
        if(v == parent) continue;
        dfs(v, u);
    }
}

void init()
{
    for(int j = 1; j <= 18; j++) //注意,不要写log2(n),会超时,log2(N)最多18
        for(int i = 1; i <= n; i++)
            fa[i][j] = fa[fa[i][j - 1]][j - 1];
}

int lca(int u, int v)
{
    if(depth[u] < depth[v]) swap(u, v);
    for(int j = 18; j >= 0; j--)
    {
        if(depth[fa[u][j]] >= depth[v]) 
        {
            u = fa[u][j];
        }
    }
    if(u == v) return u;
    for(int j = 18; j >= 0; j--)
    {
        if(fa[u][j] != fa[v][j])
        {
            u = fa[u][j];
            v = fa[v][j];
        }
    }
    return fa[u][0];
}

int main()
{
    cin >> n >> m >> s;
    int u, v;
    for(int i = 1; i < n; i++)
    {
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }
    dfs(s, 0);
    init();
    while(m--)
    {
        cin >> u >> v;
        cout << lca(u, v) << endl;
    }

    return 0;
}

方法三:Tarjan 算法

基本思想

Tarjan 算法是一种离线算法,即需要一次性处理所有的查询。它利用并查集(Union-Find)数据结构,在深度优先搜索的过程中,将已经访问过的节点合并到同一个集合中,通过回溯和并查集的查找操作来确定最近公共祖先。

具体步骤

  1. 构建查询列表
    • 对于每个查询 (u, v),将其存储在 queries[u] 和 queries[v] 中,表示 u 有一个关于 v 的查询,v 也有一个关于 u 的查询。同时记录每个查询的编号,方便后续输出结果。
  2. 深度优先搜索(DFS)与并查集操作
    • 从树的根节点开始进行深度优先搜索。在搜索过程中,标记当前节点为已访问。
    • 对于当前节点 u 的每个未访问的子节点 v,递归调用 DFS 处理 v。处理完 v 后,将 v 合并到 u 所在的集合中(使用并查集的合并操作)。
    • 对于 queries[u] 中的每个查询 (v, idx),如果 v 已经被访问过,那么通过并查集的查找操作找到 v 所在集合的代表元素,这个代表元素就是 u 和 v 的最近公共祖先,将结果存储在 ans[idx] 中。
  3. 输出结果
    • 处理完所有节点后,根据查询编号输出每个查询的最近公共祖先。

复杂度分析

  • 时间复杂度
    • 深度优先搜索需要遍历树的所有节点,时间复杂度为O(n)。
    • 并查集的合并和查找操作在路径压缩和按秩合并的优化下,平均时间复杂度接近O(1)。对于q个查询,总的时间复杂度为O(n+q)。
  • 空间复杂度:主要用于存储邻接表、查询列表和并查集数组,空间复杂度为O(n+q)。

适用场景

该方法适用于需要一次性处理大量查询的情况。由于它是离线算法,不适合动态查询的场景。但在处理批量查询时,其时间复杂度较低,性能较好。

100分代码:

#include <bits/stdc++.h>
using namespace std;

const int N = 500005;

vector<int> g[N];
vector<pair<int, int>> queries[N]; //queries[u]表示查询中所有u对应的节点first,查询编号为second
int p[N]; //并查集
int vis[N];
int n, m, s;
int ans[N];

int find(int x)
{
    if(p[x] != x) p[x] = find(p[x]);
    return p[x];
}

void tarjan(int u)
{
    vis[u] = true;
    for(auto v : g[u])
    {
        if(vis[v]) continue;
        tarjan(v);
        p[v] = u;
    }
    for(auto q : queries[u])
    {
        int v = q.first, idx = q.second;
        if(vis[v]) ans[idx] = find(v);
    }
}

int main()
{
    cin >> n >> m >> s;
    int u, v;
    for(int i = 1; i < n; i++)
    {
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }
    for(int i = 1; i <= m; i++)
    {
        cin >> u >> v;
        queries[u].push_back({v, i});
        queries[v].push_back({u, i});
    }

    for(int i = 1; i <= n; i++) p[i] = i; //并查集初始化
    tarjan(s);
    for(int i = 1; i <= m; i++) cout << ans[i] << endl;

    return 0;
}

 

posted @   五月江城  阅读(22)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
历史上的今天:
2024-03-05 洛谷题单指南-搜索-P1036 [NOIP2002 普及组] 选数
2024-03-05 洛谷题单指南-搜索-P2895 [USACO08FEB] Meteor Shower S
2024-03-05 洛谷题单指南-搜索-P1135 奇怪的电梯
点击右上角即可分享
微信分享提示