作为一名合格的 OIer ,一定要有自我总结的意识,一定要通过写博客的方式来验证自己的掌握程度

————沃.茨基硕德


今天,咱们来讲一讲LCA算法

写在前面

对于LCA,笔者也并没有说做到百分之百的掌握掌握了就不用写博客了呀,所以若有什么错误的地方,希望各位看官可以指出;

0. 一些定义

首先,我们需要明白一些定义,这些定义可以帮我们更好的理解算法

  1. 祖先:有根树中,一个节点到根的路径上的所有节点被视为这个点的祖先,包括根和它本身;
  2. 公共祖先:对于点a和b,如果c既是a的祖先又是b的祖先,那么c是a和b的公共祖先;
  3. 深度:子节点的深度=父节点深度+1,一般我们定根的深度为1;
  4. 最近公共祖先:树上两个节点的所有公共祖先中,深度最大的那个称为两个点的最近公共祖先(LCA的定义)。

好了,对于LCA的学习前提条件,各位看官已经明白了,那么接下来就正式进入LCA的讲解

1. 暴力爬山法

这个方法就和他的名字一样,暴力,变态但确实简单
如下图:

虽然有点丑将就看吧

方法

  1. 如果a和b的深度不同,那么将深度更大的那个点向根的方向移动一步(即选择它的父节点),重复这个过程直到两个点深度相同;
  2. 当a和b深度相同,但不是同一个点,那么各自向根的方向移动一步(即选择它们各自的父节点),重复这过程直到选择到同一个点。

举个栗子

那让我们来尝试一下这个方法吧!

假设我们现在要找5和7的LCA:

第一步,找到每一个点的深度(DFS);

第二步,发现7的深度比5大,所以把7换成他的父亲节点4;

第三步,5和4的深度相同,但它们不是同一个点,所以把5换成3,6也换成3;

第四步,发现3和3是相同的店,所以LCA(5,7)=3;

核心代码:

int LCA(int a,int b){
while(dis[a]!=dis[b]){ //如果深度不同,就转换为父亲节点
if(dis[a]<dis[b])
b=fa[b]; //如果b的深度大于a,就让b变成它的父亲
else
a=fa[a]; //a也同理
}
while(a!=b){
a=f[a],b=f[b]; //如果他们的深度相同,但却不是同一个点,就一直往上找父亲
}
return a; //相同则返回
}

各位客官,是否发现暴力爬山法的时间复杂度是O(n)呢?

我们将这棵树退化成一条链,查询的两点一个为根节点,另一个为叶子节点,这样的时间复杂度就退化成了 O(n),如果我们查询q次,它的复杂度就是O(nq);

但聪明的客官是否发现,我们可以将它升级成O(nlogn)呢?

这就是第二种方法了。

2.基于倍增的暴力爬山法

方法

对于第一个部分,假设a的深度小于b,那么我们在做的事实际上就是找到b的祖先中深度和a一样的点c。

对于第二个部分,我们做的事就是找到a和c的LCA,假设是点d。

这两个部分都可以运用倍增的思想优化。

预处理

先求出每个点往根的方向走2的幂步的点,和快速幂中求a的次方类似。

如果我们以dp[i][j]表示从i往根方向走2j步的节点,那么也就等于从dp[i][j-1]再往根的方向走2(j-1)步的结果,即dp[i][j]=dp[dp[i][j-1]][j-1]

可以在bfs求树上每个点深度的时候,顺便预处理fa数组,时间复杂度o(nlogn)。

一般如果走的步数超过到根的步数,一般设结果为0,因为一般点的编号不包0。

倍增优化-第一部分

我们从大到小枚举i,考虑往根的方向走2^i步之后深度会不会小于c的深度,也就是a的深度,如果不会就选择走,反之就选择不走。

时间复杂度o(logn)。

倍增优化-第二部分

假设点a、c和点d的深度差为dis,只要走的步数大于等于dis,那么a和c走的结果肯定是同一个点,所以先走dis-1步,再走一步。

所以我们要先判断现在的a和c是不是相同,如果不相同, 就先走到不相同的深度最小的点,再走一步。

同样,从大到小枚举i,每次走2^i步。

时间复杂度o(logn)。

代码

#include <bits/stdc++.h>
using namespace std;
const int Step = 20; int n, m;
vector<int> v[100005];
bool f[100005];
int pre[100005];
int dp[100005][30]; void BFS(int root) {
queue<int> q;
q.push(root);
pre[root] = 1;
while (!q.empty()) {
int x = q.front();
q.pop();
for (int i = 0; i < v[x].size(); i++) {
int y = v[x][i];
if (pre[y])
continue;
dp[y][0] = x;
pre[y] = pre[x] + 1;
for (int j = 1; j <= Step; j++) { //dp数组维护
dp[y][j] = dp[dp[y][j - 1]][j - 1];
}
q.push(y);
}
}
} int LCA(int x, int y) {
if (pre[x] > pre[y]) { //便于操作,深度大的放前面
int t = x;
x = y;
y = t;
}
for (int i = Step; i >= 0; --i) { //找到和x深度相同的点
if (pre[dp[y][i]] >= pre[x])
y = dp[y][i];
}
if (x == y)
return x;
for (int i = Step; i >= 0; --i) { //找到值相同的点
if (dp[x][i] != dp[y][i]) {
x = dp[x][i];
y = dp[y][i];
}
}
return dp[x][0];
} int main() {
cin >> n >> m;
for (int i = 1; i < n; i++) {
int x, y;
scanf("%d%d", &x, &y);
v[x].push_back(y);
v[y].push_back(x);
}
BFS(1);
for (int i = 1; i <= m; i++) {
int x, y;
scanf("%d%d", &x, &y);
printf("%d\n", LCA(x, y));
}
return 0;
}

3.Tarjan算法

方法

Tarjan算法本质上是用并查集的路径压缩优化的dfs,时间复杂度为O(N + Q)。

在DFS的任意时刻,树中节点分为两类:

  1. 已经开始递归的节点;
  2. 尚未访问的节点。

我们可以定义一个布尔类型的数组f,在这些1类上标记一个整数1,2类节点暂时不管。

在DFS的回溯时,我们可以把回溯点的并查集中的祖先设为他的父节点

那么对于任何两个点来说,如果两个点都被递归遍历过,那么他们的LCA就是两个点并查集中的祖先。

举个栗子


我们来找6和10的LCA;

  1. 搜索,将f[2]设为1;
  2. f[3]=1;
  3. f[4]=1;
  4. f[6]=1,发现查询中有6,但又发现10并未被查询(即f[10]=0),所以暂时不管;
  5. 6为叶子节点,回溯,并让pre[6]=4;
  6. f[7]=1;
  7. 7为叶子节点,回溯,并让pre[7]=4;
  8. 4所有节点都遍历过了,回溯,并让pre[4]=3;
  9. f[5]=1;
  10. ……
  11. f[10]=1,发现查询中有10,且6已被查询过(f[6]=1),所以LCA(6,10)=Find_Set(6)=3;

代码

#include <bits/stdc++.h>
using namespace std; struct zz {
int u, id;
}; vector<int> v[100005];
vector<zz> sum[100005];
int n, m;
int ans[100005];
int pre[100005];
bool f[100005]; int Find_Set(int x) {
if (x != pre[x]) {
pre[x] = Find_Set(pre[x]);
}
return pre[x];
} void Make_Set(int x) {
for (int i = 1; i <= x; i++) {
pre[i] = i;
}
} void Tarjan(int x) {
f[x] = 1;
for (int i = 0; i < v[x].size(); i++) {
int y = v[x][i];
if (f[y])
continue;
Tarjan(y);
pre[y] = Find_Set(x);
}
for (int i = 0; i < sum[x].size(); i++) {
zz y = sum[x][i];
if (f[y.u] == 1) {
ans[y.id] = Find_Set(y.u);
}
}
} int main() {
cin >> n >> m;
Make_Set(n);
for (int i = 1; i < n; i++) {
int x, y;
scanf("%d%d", &x, &y);
v[x].push_back(y);
v[y].push_back(x);
}
for (int i = 1; i <= m; i++) {
int x, y;
scanf("%d%d", &x, &y);
sum[x].push_back(zz{ y, i });
sum[y].push_back(zz{ x, i });
}
Tarjan(1);
for (int i = 1; i <= m; i++) {
printf("%d\n", ans[i]);
}
return 0;
}

LCA总结的更多相关文章

  1. BZOJ 3083: 遥远的国度 [树链剖分 DFS序 LCA]

    3083: 遥远的国度 Time Limit: 10 Sec  Memory Limit: 1280 MBSubmit: 3127  Solved: 795[Submit][Status][Discu ...

  2. BZOJ 3626: [LNOI2014]LCA [树链剖分 离线|主席树]

    3626: [LNOI2014]LCA Time Limit: 10 Sec  Memory Limit: 128 MBSubmit: 2050  Solved: 817[Submit][Status ...

  3. [bzoj3123][sdoi2013森林] (树上主席树+lca+并查集启发式合并+暴力重构森林)

    Description Input 第一行包含一个正整数testcase,表示当前测试数据的测试点编号.保证1≤testcase≤20. 第二行包含三个整数N,M,T,分别表示节点数.初始边数.操作数 ...

  4. [bzoj2588][count on a tree] (主席树+lca)

    Description 给定一棵N个节点的树,每个点有一个权值,对于M个询问(u,v,k),你需要回答u xor lastans和v这两个节点间第K小的点权.其中lastans是上一个询问的答案,初始 ...

  5. [板子]倍增LCA

    倍增LCA板子,没有压行,可读性应该还可以.转载请随意. #include <cstdio> #include <cstring> #include <algorithm ...

  6. poj3417 LCA + 树形dp

    Network Time Limit: 2000MS   Memory Limit: 65536K Total Submissions: 4478   Accepted: 1292 Descripti ...

  7. [bzoj3626][LNOI2014]LCA

    Description 给出一个$n$个节点的有根树(编号为$0$到$n-1$,根节点为$0$). 一个点的深度定义为这个节点到根的距离$+1$. 设$dep[i]$表示点$i$的深度,$lca(i, ...

  8. (RMQ版)LCA注意要点

    inline int lca(int x,int y){ if(x>y) swap(x,y); ]][x]]<h[rmq[log[y-x+]][y-near[y-x+]+]])? rmq[ ...

  9. bzoj3631: [JLOI2014]松鼠的新家(LCA+差分)

    题目大意:一棵树,以一定顺序走完n个点,求每个点经过多少遍 可以树链剖分,也可以直接在树上做差分序列的标记 后者打起来更舒适一点.. 具体实现: 先求x,y的lca,且dep[x]<dep[y] ...

  10. 在线倍增法求LCA专题

    1.cojs 186. [USACO Oct08] 牧场旅行 ★★   输入文件:pwalk.in   输出文件:pwalk.out   简单对比时间限制:1 s   内存限制:128 MB n个被自 ...

随机推荐

  1. [刷题] 198 House Robber

    要求 你是一个小偷,每个房子中有价值不同的宝物,但若偷连续的两栋房子,就会触发报警系统,求最多可偷价值多少的宝物 示例 [3,4,1,2],返回6[3,(4),1,(2)] [4,3,1,2],返回6 ...

  2. nano 按Ctrl+X 输入Y 回车

    如何退出nano 1.nano 按Ctrl+X 如果你修改了文件,下面会询问你是否需要保存修改. 2.输入Y确认保存,输入N不保存,按Ctrl+C取消返回.如果输入了Y,下一步会让你输入想要保存的文件 ...

  3. NFS PersistentVolume(11)

    一.部署nfs服务端 1.需在 k8s-master 节点上搭建了一个 NFS 服务器,目录为 /nfsdata: yum install -y nfs-utils rpcbind vim /etc/ ...

  4. JavaSE 知识图谱

    JAVA基础语法 DOS命令 JAVA介绍 JDK安装 JAVA环境的搭建 关键字 注释 标识符命名规则(编码规范) 字面值常量 进制转换 基本类型 变量(局部变量.静态变量) 运算符 表达式 控制语 ...

  5. linux python3安装whl包时报错解决:is not a supported wheel on this platform

    原因1 你下载安装的包不是当前平台所支持的 原因2 你下载的包,不符合你所在的平台的安装whl的名称规范,所以出错.比如当前我要安装的包是:pymssql-2.1.5-cp36-cp36m-manyl ...

  6. 安卓开发(2)—— Kotlin语言概述

    安卓开发(2)-- Kotlin语言概述 Android的官方文档都优先采用Kotlin语言了,学它来进行Android开发已经是一种大势所趋了. 这里只讲解部分的语法. 如何运行Kotlin代码 这 ...

  7. Nacos源码结构和AP模式注册中心实现介绍

    前言 NacosAP模式源码分析目录 微服务下的注册中心如何选择 Nacos使用和注册部分源码介绍 Nacos服务心跳和健康检查源码介绍 Nacos服务发现 Nacos源码结构介绍 Nacos版本基于 ...

  8. C语言编译器开发之旅(开篇)

    编译器写作之旅   最近在Github上看到一个十分有趣的项目acwj(A Compiler Writing Journey),一个用C语言编写编译器的项目.身为一个程序员,这在我看来是一件十分酷的事 ...

  9. 书列荐书 |《刻意练习》安德斯&#183;艾利克森,罗伯特&#183;普尔著

    花了两天的时间,一气呵成的读完了这本书.凝练的精华就是:首先,世界上并没有真正的天才这一说.基因可能会起作用,但是经过后天大量的刻意练习,基因的这种作用会弱化.刻意练习需要专注.及时的反馈,并根据反馈 ...

  10. 机器学习PAL产品优势

    机器学习PAL产品优势 PAI支持丰富的机器学习算法.一站式的机器学习体验.主流的机器学习框架及可视化的建模方式.本文介绍PAI的产品优势. 丰富的机器学习算法 PAI的算法都经过阿里巴巴集团大规模业 ...