最近公共祖先(LCA)

最近,公共,祖先,lca · 浏览次数 : 8

小编点评

The provided code calculates the shortest path between two nodes in a tree using Tarjan's algorithm. The code takes the tree structure and the distances between each pair of nodes as input and then performs a DFS on the tree to find the shortest path between all pairs of nodes. **Key Points:** * The code uses Tarjan's algorithm, an offline algorithm that determines the shortest path between two nodes in a tree. * It maintains a union-find data structure to keep track of the connected components of the tree. * During the DFS, it marks the nodes in the current subtree as visited and recursively explores their child nodes. * It maintains a variable `ans` to store the shortest path between each pair of nodes. * For each query, it finds the nearest common ancestor of the two nodes and updates the shortest path in the `ans` array. **Code Outline:** 1. Initialize the `ans` array to 0 for all nodes. 2. Perform a DFS on the tree, marking nodes as visited and exploring their child nodes. 3. For each visited node, mark its children as visited and recursively explore them. 4. Update the `ans` array to store the shortest path between all pairs of nodes. 5. Process queries to find the shortest path between two nodes. **Pseudocode:** ```python def tarjan(u): # Initialize visited array vis[u] = True # Perform DFS on children of u for v in neighbors[u]: if not vis[v]: tarjan(v) marge(u, v) # Return the shortest path between u and its ancestor return ans[u] def merge(u, v): # Add u and v's ancestors to the same component # using the union-find data structure root_u = find(u) root_v = find(v) if root_u != root_v: union(root_u, root_v) # Function to find the nearest common ancestor def lca(u, v): # Traverse from u to the root of u's component ancestors_u = find(u) ancestors_v = find(v) # Return the ancestor of u that is also an ancestor of v return ancestors_u if ancestors_u == ancestors_v else ancestors_u ``` **Additional Notes:** * The code assumes that the input tree is a valid binary tree. * The `neighbors` array stores the neighboring nodes of each node. * The `find` function identifies the root node of the component containing a given node.

正文

什么是LCA

最近公共祖先是相对于两个节点来说的,顾名思义,最近公共祖先即为两个节点公共的最近的祖先。

image

如上图,\(86\)\(67\)\(LCA\)\(75\)\(67\)\(27\)\(LCA\)\(50\)

怎么求LCA

倍增法

我们先想想暴力怎么做:从这两个节点开始,一步一步往上跳,直到跳到了一起,那么就是 \(LCA\) 了。

那我们该怎么优化暴力呢?我们可以一次跳多个节点呀!如果我们在每一个节点上,都先预处理出 \(log(n)\) 个节点信息,分别表示向上走 \(1\) 步,\(2\) 步,\(4\) 步,\(8\) 步,\(\dots\)\(2^k\) 步所到达的节点。这样我们只需要 \(log(n)\) 步就可以走到根节点。

然后仍然套用之前暴力的方法向上走即可。

image

上面树的节点信息如下:

节点 节点信息
\(19\) \(1\) 步:\(15\);跳 \(2\) 步:\(9\);跳 \(4\) 步:\(1\)
\(20\) \(1\) 步:\(16\);跳 \(2\) 步:\(10\);跳 \(4\) 步:\(1\)
\(16\) \(1\) 步:\(10\);跳 \(2\) 步:\(4\)
\(10\) \(1\) 步:\(4\);跳 \(2\) 步:\(1\)

如何记录这些节点信息呢?

初始我们只知道向上走 \(1\) 步的信息。

然后根据走 \(1\) 步的信息,推出走 \(2\) 的信息。

再根据走 \(2\) 步的信息,推出走 \(4\) 步的信息。

\(\dots\)

很明显我们通过递推就可以求出来了。

我们用数组 \(f[u][k]\) 表示节点 \(u\) 向上走 \(2^k\) 步所走到的点,显然有 \(f[u][0] = u\) 的父亲节点,则我们的递推式则为:

\(f[u][k] = f[f[u][k - 1]][k - 1]\)

for (int k = 1; k <= 20; ++k)
    for (int u = 1; u <= n; ++u)
        f[u][k] = f[f[u][k - 1]][k - 1];

在求 \(x,y\) 两个节点的 \(LCA\) 时,如果 \(x,y\) 深度不同,则先让深度较大的节点向上跳到另一个节点所在的深度。记 \(dep[u]\) 表示节点 \(u\) 的深度,假设 \(dep[x] > dep[y]\),那么先让 \(x\) 倍增的向上跳 \(dep[x] - dep[y]\) 步。

int tmp = dep[x] - dep[y];
for (int i = 20; i >= 0; --i)
    if (tmp & (1 << i))
        x = f[x][i];

当两个节点深度相同时,就同时倍增的向上跳,但是又不能让他们跳过 \(LCA\)。具体来说,我们从大到小枚举 \(k\),判断 \(x, y\) 同时向上走 \(2 ^ j\) 步是否会相遇,如果不会,则向上跳 \(2 ^ j\) 步。重复这个过程,此时 \(x, y\) 就会跳到 \(LCA\) 的子儿子处,此时再进一步就是 \(LCA\)

for (int k = 20; k >= 0; --k)
    if (f[x][k] != f[y][k])
        x = f[x][k], y = f[y][k];
if (x != y) x = f[x][0], y = f[y][0];
// 此时x和y都是LCA

题目:

image

举个例子:

image

如果我们记录了每个点到根节点的边权和 \(dis_i\),则从 \(x\)\(y\) 的路径边权和即为

\(dis_i + dis_j - 2 \times dis_{LCA(i, j)}\)

代码:

#include <bits/stdc++.h>
#define N 100010
using namespace std;
int n, m;
int h[N], e[N * 2], w[N * 2], ne[N * 2], idx;
bool vis[N];
int dis[N], dep[N];
int f[N][30], fa[N];
void add(int a, int b, int c) { e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++; }
void dfs(int u, int father) {
	fa[u] = father;
    for (int i = h[u]; i != -1; i = ne[i]) {
        int j = e[i];
        if (j != father) {
        	dep[j] = dep[u] + 1;
        	dis[j] = dis[u] + w[i];
        	dfs(j, u);
        }
    }
}
int lca(int x, int y) {
    if (dep[x] < dep[y]) swap(x, y);
    int tmp = dep[x] - dep[y];
    for (int k = 20; k >= 0; --k)
        if (tmp & (1 << k))
            x = f[x][k];
    for (int k = 20; k >= 0; --k)
        if (f[x][k] != f[y][k])
            x = f[x][k], y = f[y][k];
    if (x != y) x = f[x][0], y = f[y][0];
    return x;
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    memset(h, -1, sizeof h);
    cin >> n >> m;
    for (int i = 1; i <= n - 1; ++i) {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c), add(b, a, c);
    }
    dfs(1, 0);
    for (int i = 1; i <= n; ++i) f[i][0] = fa[i];
    for (int k = 1; k <= 20; ++k)
        for (int u = 1; u <= n; ++u)
            f[u][k] = f[f[u][k - 1]][k - 1];
    while (m--) {
        int x, y;
        cin >> x >> y;
        cout << dis[x] + dis[y] - 2 * dis[lca(x, y)] << '\n';
    }
    return 0;
}

Tarjan

\(Tarjan\) 算法也是可以求 \(LCA\) 的,但是它是一种离线算法。所谓离线算法,就是说它不会在查询两个节点的 \(LCA\) 时直接给出答案,而是等到所有查询操作都给出后统一进行求解。

相比于倍增法,\(Tarjan\) 的效率很高,时间复杂度最低可以达到 \(O(n + q) \times 并查集的复杂度\)

由于作者不会按秩合并,所以大家时间如果时间充裕的话并查集用个路径压缩就行了。

我们考虑以下过程:

在对一棵树进行 \(DFS\) 的过程中,对于一对点 \(x\)\(y\),我们肯定是先遍历到一个,再后遍历到一个。

不妨假设先遍历到 \(x\),再遍历到 \(y\)

当我们遍历完 \(x\) 时,我们肯定会回溯到 \(x\) 的祖先节点 \(p\),然后接着遍历其祖先节点的其它子树,直到遍历到某一个子树里遍历到 \(y\) 了,我们惊喜的发现,\(p\) 就是 \(x\)\(y\) 的最近公共祖先了。

具体来说,\(Tanjan\) 的算法流程如下:

  1. 维护一个并查集。
  2. 任选一个节点作为根节点进行 \(DFS\)
  3. 当我们遍历到一个点 \(u\) 时,遍历其所有子节点 \(v\),并将 \(v\) 标记为已访问,接着遍历 \(v\)
  4. 合并 \(u\)\(v\) 的并查集,将 \(find(v)\) 作为并查集的子节点连在 \(find(u)\) 的下面(使得深度更浅的节点作为并查集的根)。
  5. 处理与 \(u\) 节点相关的询问 \(LCA(u, x)\),如果 \(x\) 已经被访问过,那么 \(LCA(u, x)\) 就是 \(find(x)\)

伪代码:

tarjan(u) {
  for (u -> v) { // 遍历u所有有子节点v
    vis[v] = true; // 标记为已访问
    tarjan(v); // 继续向下遍历
    marge(u, v); // 合并并查集
  }
  for (query(u, x)) { // 处理所有与u节点相关的询问 
    if (vis[x]) ans[(u, x)] = find(x); // 给出答案
  }
}

与最近公共祖先(LCA)相似的内容:

最近公共祖先(LCA)

## 什么是LCA 最近公共祖先是相对于两个节点来说的,顾名思义,最近公共祖先即为两个节点公共的最近的祖先。 ![image](https://img2023.cnblogs.com/blog/3257810/202308/3257810-20230812133415742-926455766.pn

算法学习笔记(5): 最近公共祖先(LCA)

树上最近公共祖先(LCA)三种求法:倍增,DFS+ST表,熟练剖分

算法学习笔记(6): 树链剖分

树链剖分 树链剖分是一个很神奇,但是在树上可以完成一些区间操作问题 简单来说,就是把一棵树分成一条条的链,通过维护链上的信息来维护整棵树的信息 基础知识可以参考我的另外一篇博客:算法学习笔记(5): 最近公共祖先(LCA) 这里假设你已经掌握了上述博客中的所有相关知识,并清晰了其背后的原理 性质?发

剑指 Offer 68 - I. 二叉搜索树的最近公共祖先(java解题)

leetcode《图解数据结构》剑指 Offer 68 - I. 二叉搜索树的最近公共祖先(java解题)的解题思路和java代码,并附上java中常用数据结构的功能函数。

剑指 Offer 68 - II. 二叉树的最近公共祖先(java解题)

leetcode《图解数据结构》剑指 Offer 68 - II. 二叉树的最近公共祖先(java解题)的解题思路和java代码,并附上java中常用数据结构的功能函数。

C#使用SendMessage进行进程间通讯

最近公司有个需求是,拖动文件到桌面图标上,自动打开文件。那么只需在OnStartup事件中通过StartupEventArgs获取文件名然后进行操作即可。操作之后发现当软件已经启动了(单例运行),那么将无法将参数传给业务层。原因是因为跨进程了,那么我们可以通过窗口句柄的方式来进行通讯。 1 publ

kkfileview搭建指南

最近公司有个需求,需要在线预览pdf,excel,world文档,pdf浏览器是直接支持预览的,vue也有很多插件支持,但是world文档和excel的方案就非常少了,市面上很多付费的,但是咱一般不舍得花钱,所以找了很多方案最后发现一款很吊的开源应用 kkfileview,这是作者的博客地址 htt

Windows 修改时间提示: 某些设置已隐藏或由你的组织管理 的解决方案

最近公司的一台生产服务器时间不对. 因为机器有域控的需求, 所以加入了域, 想改时间时有这样的提示信息: 某些设置已隐藏或由你的组织管理 百度了很久发现没有解决方法.. 但是突然发现可以使用 运行->cmd 或者是powershell 运行命令: timedate.cpl 的方式进行修改. 问题解决

最长公共子序列问题

最长公共子序列问题 作者:Grey 原文地址: 博客园:最长公共子序列问题 CSDN:最长公共子序列问题 题目描述 给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。 一个字符串的 子序列 是指这样一个新的字符串:它是由原字

git实战

最近公司又来一批小伙伴,对git的使用非常陌生,我就安排给大家讲了下git的基本使用,今天也总结下发到博客园上和大家分享 一、git安装 由于公司都是用windows,本屌丝也是用windows,所有这里就只讲windows的安装 windows的安装非常简单 1、下载git:https://git