研究了LCA,写篇笔记记录一下。

讲解使用例题 P3379 【模板】最近公共祖先(LCA)

什么是LCA

最近公共祖先简称 LCA(Lowest Common Ancestor)。两个节点的最近公共祖先,就是这两个点的公共祖先里面,离根最远的那个。

—— 摘自 OI Wiki

比如下图红、黄两点的LCA就是绿点。

LCA的几种实现方式

向上标记法

从 x 点一直向上走直到到达根节点,在走的过程中标记所有经过的点。

从 y 点一直向根节点走,遇到的第一个标记过的点即为两点的LCA。

代码略

树上倍增法

首先,我们将要求、lca的两点跳到同一深度,如下图:

然后两点同时向上从大到小倍增,直到到的两点不相同,继续往上跳。

先尝试向能跳的最远处跳(4步)。

我们发现两个点在同处汇合,不行,考虑少跳一半(2步)。

不同点,跳上。继续少跳一半(1步)。

同一个点,不跳。

此时,所有的跳跃尝试结束。由于目前两点不在同处,故再往上跳一步。

于是就找到这两个点的LCA啦!

(是不是讲的云里雾里的,结合代码理解一下吧~)

代码实现

  • dfs获取每个点的深度
int p[N], dep[N];
void dfs(int x, int f) {
p[x] = f;
for (int i = last[x]; i; i = e[i].next) { //我用邻接表存的图
int v = e[i].to;
if (v == f) continue;
dep[v] = dep[x] + 1;
dfs(v, x);
}
}
dep[s] = 1;
dfs(s, s); //将起点的父节点设为自己,这样跳多了也不会出锅
  • 预处理倍增跳到的点
for (int i = 1; i <= n; i++) f[0][i] = p[i];
for (int j = 1; j <= lg; j++) // 跳 2^j 步 lg 为 log2(n)
for (int i = 1; i <= n; i++) // 第 i 个点
f[j][i] = f[j - 1][f[j - 1][i]];
// 跳 2^j 步到的点即为先跳 2^(j-1) 步再跳 2^(j-1) 步到的点
  • 处理LCA

(没有写成函数QAQ)

int a = read(), b = read();
if (dep[a] > dep[b]) swap(a, b); //使 a 的深度小于等于 b
for (int i = lg; i >= 0; i--)
if (dep[f[i][b]] >= dep[a]) b = f[i][b]; //将 a 与 b 跳到同一深度
for (int i = lg; i >= 0; i--) //从最远的距离开始尝试 (跳 2^i 步)
if (f[i][b] != f[i][a]) b = f[i][b], a = f[i][a]; //不是同一个点就跳上去
if (a != b) a = p[a];
//结束后不是同一个点,那么LCA就是目前这个点的父节点,所以也可以写成 b = p[b] 然后输出 b
printf("%d\n", a);

  • 为什么尝试跳只用从 log2(n) 循环一遍到 0 就行?

按照代码思路,我们会先尝试沿紫色路径跳 2^j 步,由于不成功,我们折半跳 2^(j-1) 步,沿粉边跳上。

此时若在沿蓝边跳 2^(j-1) 步,又跳到了原来粉边指向的点,我们已经知道那个点不行,所以不用尝试跳上,而应该继续尝试跳 2^(j-2) 步。


完整代码(点击查看)
#include<bits/stdc++.h>
using namespace std;
#define ll long long
inline ll read() {
ll s = 0, w = 1;
char ch = getchar();
while (ch < '0' || ch > '9'){if (ch == '-') w = -1; ch = getchar();}
while (ch >= '0' && ch <= '9'){s = (s << 3) + (s << 1) + (ch ^ 48); ch = getchar();}
return s * w;
}
const int N = 500010;
int n, m, s;
int last[N], cnt;
struct edge {
int to, next;
} e[N << 1];
void addedge(int x, int y) {
e[++cnt].to = y;
e[cnt].next = last[x];
last[x] = cnt;
}
int p[N], dep[N];
void dfs(int x, int f) {
p[x] = f;
for (int i = last[x]; i; i = e[i].next) {
int v = e[i].to;
if (v == f) continue;
dep[v] = dep[x] + 1;
dfs(v, x);
}
}
int f[19][N], lg;
int main() {
n = read(), m = read(), s = read();
lg = log2(n);
for (int i = 1; i < n; i++) {
int u = read(), v = read();
addedge(u, v), addedge(v, u);
}
dep[s] = 1;
dfs(s, s);
for (int i = 1; i <= n; i++) f[0][i] = p[i];
for (int j = 1; j <= lg; j++)
for (int i = 1; i <= n; i++)
f[j][i] = f[j - 1][f[j - 1][i]];
while (m--) {
int a = read(), b = read();
if (dep[a] > dep[b]) swap(a, b);
for (int i = lg; i >= 0; i--)
if (dep[f[i][b]] >= dep[a]) b = f[i][b];
for (int i = lg; i >= 0; i--)
if (f[i][b] != f[i][a]) b = f[i][b], a = f[i][a];
if (a != b) a = p[a];
printf("%d\n", a);
}
return 0;
}

LCA的Tarjan算法

本质来说,其实就是用并查集对“向上标记法”进行优化。

注意:操作是离线的。

从根节点开始进行 DFS,对于每个搜到的点打上标记,在回溯时将该结点并入其父节点的集合,具体操作见下。


  • 如何离线?

我们先把 m 次询问都读入,然后再相关的两个结点上分别挂上询问。

  • 为什么要两点都挂上询问

因为我们并不知道两个点谁先访问谁后访问,不好处理。


比如现在给一棵树,询问红、黄两点的 LCA 。

我们对这棵树进行 DFS,目前已经搜到了黄点,上方的三个不同深度的橙点表示 DFS 过程中栈里的点。

由于已经搜过了根节点的左子树,所以红点已打过标记。根节点的左子树与根节点属于一个集合,第二层的黄点的左子树与它自己属于一个集合。

现在在黄点上打个标记,发现黄点上挂的关于红点的询问可以处理了(两点都已搜到)。

红、黄两点的LCA即为红点所在集合的根节点,即图中树的根节点。

(讲的有亿点点乱诶)

代码实现

  • 存储询问
struct node { //为了保证输出顺序,不仅要把询问挂在点上,还要额外存一下
int x, y, ans;
} ask[N];
vector <int> g[N]; //每个点上挂的询问
for (int i = 1; i <= m; i++) {
ask[i].x = read(), ask[i].y = read(), ask[i].ans = -1;
g[ask[i].x].push_back(i);
g[ask[i].y].push_back(i);
}
  • DFS
int p[N];
bool vis[N]; //访问标记
int r[N]; //一个集合实际的根节点(并查集是按秩合并的,根节点不能保证是我们要的根节点)
void dfs(int x, int f) {
p[x] = f;
for (int i = last[x]; i; i = e[i].next) {
int v = e[i].to;
if (v == f) continue;
vis[v] = 1;
for (int j : g[v]) { //遍历所有询问
int o = ask[j].x;
if (o == v) o = ask[j].y;
if (!vis[o]) continue;
ask[j].ans = r[a.root(o)]; //记录询问答案
}
dfs(v, x);
a.merge(x, v); //合并两个集合
r[a.root(x)] = x; //标记实际根节点
}
}
vis[s] = 1;
dfs(s, s);
完整代码(点击查看)
#include<bits/stdc++.h>
using namespace std;
#define ll long long
inline ll read() {
ll s = 0, w = 1;
char ch = getchar();
while (ch < '0' || ch > '9'){if (ch == '-') w = -1; ch = getchar();}
while (ch >= '0' && ch <= '9'){s = (s << 3) + (s << 1) + (ch ^ 48); ch = getchar();}
return s * w;
}
const int N = 500010;
int n, m, s;
struct Disjoint_Set {
int p[N], size[N];
void build() {
for (int i = 1; i <= n; i++) p[i] = i, size[i] = 1;
}
int root(int x) {
if (p[x] != x) return p[x] = root(p[x]);
return x;
}
void merge(int x, int y) {
x = root(x), y = root(y);
if (size[x] > size[y]) swap(x, y);
p[x] = y;
size[y] += size[x];
}
bool check(int x, int y) {
x = root(x), y = root(y);
return x == y;
}
} a;
int last[N], cnt;
struct edge {
int to, next;
} e[N << 1];
void addedge(int x, int y) {
e[++cnt].to = y;
e[cnt].next = last[x];
last[x] = cnt;
}
struct node {
int x, y, ans;
} ask[N];
vector <int> g[N];
int p[N];
bool vis[N];
int r[N];
void dfs(int x, int f) {
p[x] = f;
for (int i = last[x]; i; i = e[i].next) {
int v = e[i].to;
if (v == f) continue;
vis[v] = 1;
for (int j : g[v]) {
int o = ask[j].x;
if (o == v) o = ask[j].y;
if (!vis[o]) continue;
ask[j].ans = r[a.root(o)];
}
dfs(v, x);
a.merge(x, v);
r[a.root(x)] = x;
}
}
int main() {
n = read(), m = read(), s = read();
a.build();
for (int i = 1; i <= n; i++) {
r[i] = i;
}
for (int i = 1; i < n; i++) {
int u = read(), v = read();
addedge(u, v), addedge(v, u);
}
for (int i = 1; i <= m; i++) {
ask[i].x = read(), ask[i].y = read(), ask[i].ans = -1;
g[ask[i].x].push_back(i);
g[ask[i].y].push_back(i);
}
vis[s] = 1;
dfs(s, s);
for (int i = 1; i <= m; i++) printf("%d\n", ask[i].ans);
return 0;
}

LCA转RMQ

先贴代码吧,讲解后续再补

咕咕咕

完整代码(点击查看)
#include<bits/stdc++.h>
using namespace std;
#define ll long long
inline ll read() {
ll s = 0, w = 1;
char ch = getchar();
while (ch < '0' || ch > '9'){if (ch == '-') w = -1; ch = getchar();}
while (ch >= '0' && ch <= '9'){s = (s << 3) + (s << 1) + (ch ^ 48); ch = getchar();}
return s * w;
}
const int N = 500010;
int n, m, s;
int last[N], cnt;
struct edge{
int to, next;
} e[N << 1];
void addedge(int x, int y) {
e[++cnt].to = y;
e[cnt].next = last[x];
last[x] = cnt;
}
int dep[N], a[N << 1], ed, fst[N];
void dfs(int x, int f) {
a[++ed] = x;
if (!fst[x]) fst[x] = ed;
for (int i = last[x]; i; i = e[i].next) {
int v = e[i].to;
if (v == f) continue;
dep[v] = dep[x] + 1;
dfs(v, x);
a[++ed] = x;
}
}
int f[21][N << 1], lg;
int main() {
n = read(), m = read(), s = read();
lg = log2(n) + 1;
for (int i = 1; i < n; i++) {
int x = read(), y = read();
addedge(x, y), addedge(y, x);
}
dep[s] = 1;
dfs(s, s);
for (int i = 1; i <= ed; i++) f[0][i] = i;
for (int j = 1; j <= lg; j++) {
for (int i = 1; i <= ed - (1 << j) + 1; i++) {
int i2 = i + (1 << (j - 1));
if (dep[a[f[j - 1][i]]] < dep[a[f[j - 1][i2]]]) f[j][i] = f[j - 1][i];
else f[j][i] = f[j - 1][i2];
}
}
for (int i = 1; i <= m; i++) {
int x = read(), y = read();
if (fst[x] > fst[y]) swap(x, y);
int len = fst[y] - fst[x] + 1, ans;
int lg2 = log2(len);
int i2 = fst[y] - (1 << lg2) + 1;
if (dep[a[f[lg2][fst[x]]]] < dep[a[f[lg2][i2]]]) ans = a[f[lg2][fst[x]]];
else ans = a[f[lg2][i2]];
printf("%d\n", ans);
}
return 0;
}

最近公共祖先(LCA)学习笔记 | P3379 【模板】最近公共祖先(LCA)题解的更多相关文章

  1. OpenCV 学习笔记(模板匹配)

    OpenCV 学习笔记(模板匹配) 模板匹配是在一幅图像中寻找一个特定目标的方法之一.这种方法的原理非常简单,遍历图像中的每一个可能的位置,比较各处与模板是否"相似",当相似度足够 ...

  2. Python Flask学习笔记之模板

    Python Flask学习笔记之模板 Jinja2模板引擎 默认情况下,Flask在程序文件夹中的templates子文件夹中寻找模板.Flask提供的render_template函数把Jinja ...

  3. poj1330 lca 最近公共祖先问题学习笔记

    首先推荐两个博客网址: http://dongxicheng.org/structure/lca-rmq/ http://scturtle.is-programmer.com/posts/30055. ...

  4. Angular 5.x 学习笔记(1) - 模板语法

    Angular 5.x Template Syntax Learn Note Angular 5.x 模板语法学习笔记 标签(空格分隔): Angular Note on github.com 上手 ...

  5. LCA学习笔记

    写在前面 目录 一.LCA的定义 二.暴力法求LCA 三.倍增法求LCA 四.树链剖分求LCA 五.LCA典型例题 题目完成度 一.LCA的定义 LCA指的是最近公共祖先.具体地,给定一棵有根树,若结 ...

  6. 倍增求LCA学习笔记(洛谷 P3379 【模板】最近公共祖先(LCA))

    倍增求\(LCA\) 倍增基础 从字面意思理解,倍增就是"成倍增长". 一般地,此处的增长并非线性地翻倍,而是在预处理时处理长度为\(2^n(n\in \mathbb{N}^+)\ ...

  7. LCA 学习算法 (最近的共同祖先)poj 1330

    Nearest Common Ancestors Time Limit: 1000MS   Memory Limit: 10000K Total Submissions: 20983   Accept ...

  8. 倍增LCA学习笔记

    前言 ​ "倍增",作为一种二进制拆分思想,广泛用于各中算法,如\(ST\)表,求解\(LCA\)等等...今天,我们仅讨论用该思想来求解树上两个节点的\(LCA\)(最近公共祖先 ...

  9. leetcood学习笔记-14*-最长公共前缀

    笔记: python if not   判断是否为None的情况 if not x if x is None if not x is None if x is not None`是最好的写法,清晰,不 ...

随机推荐

  1. 867. Transpose Matrix - LeetCode

    Question 867. Transpose Matrix Solution 题目大意:矩阵的转置 思路:定义一个转置后的二维数组,遍历原数组,在赋值时行号列号互换即可 Java实现: public ...

  2. MySQL基准测试工具

    一.基准测试 基准测试(benchmark)是针对系统设计的一种压力测试. 基准测试是简化了的压力测试. 1.1 常见指标 TPS QPS 响应时间 并发量 1.2 收集与分析数据脚本 收集数据的sh ...

  3. 一条更新SQL的内部执行及日志模块

    一条更新SQL的内部执行 学习MySQL实战45讲,非常推荐学 还是老图: 上文复习 在执行查询语句的时候,会执行连接器(总要连上才能搞事情),然后去查询缓存(MySQL8+删除了),有数据返回,没数 ...

  4. 12┃音视频直播系统之 WebRTC 实现1对1直播系统实战

    一.搭建 Web 服务器 前面我们已经实现过,但是没有详细说HTTPS服务 首先需要引入了 express 库,它的功能非常强大,用它来实现 Web 服务器非常方便 同时还需要引入 HTTPS 服务, ...

  5. 20 HTTP 长连接与短连接

    20 HTTP 长连接与短连接 每日一句 纸上得来终觉浅,绝知此事要躬行. 每日一句 Never give up until the fight is over. 永远不要放弃,要一直战斗到最后一秒. ...

  6. 很好用的vscode 插件 Open PHP/HTML/JS In Browser 让php文件直接在浏览器打开

    p { font-size: 25px } <body> <h1>安装插件</h1> <img src="https://img2020.cnblo ...

  7. JavaDoc——JavaSE基础

    JavaDoc 文档注释内容的含义 @author // 作者 @version // 版本 @since // 最早支持的Java版本 @param // 接收的参数 @return // 返回值 ...

  8. 微前端(qiankun)主应用共享React组件

    前言 最近需要重构一个老项目,定的方案用微前端去改造.主应用是老的项目,微应用是新的项目,由于重构时间比较紧张,子应用还需要使用父应用的一些组件.过程中遇到一些问题,记录一下. 方案 我们知道qian ...

  9. Swoole一键操作基于阿里云的RDS数据库迁移+OSS文件搬迁

    传统的数据库搬迁思路是把数据库表的结构及数据都查询出来,然后通过循环进行数据结构重组拼接.然后导出!数据量少的话,这样当然是没毛病.当数据量太大的时候,服务器的内存开销就吃不住了,很容易炸掉,导致服务 ...

  10. Linux基础命令、引号和括号的作用

    查看硬件信息 查看 cpu lscpu命令可以查看cpu信息 cat /proc/cpuinfo也可看查看到 查看内存大小 free命令 cat /proc/meminfo 查看硬盘和分区 lsblk ...