最近公共祖先(LCA)

最近公共祖先是树上的概念,不了解树的出门左转百度:树(数据结构名词)_百度百科

定义

假设我们需要求 x 和 y 的最近公共祖先,这里有多种等价的定义

  • 路径x到y上深度最小的点

  • x和y公共祖先中深度最大的点

  • x和y在这棵树上距离最近的公共祖先结点

如果x就是y的祖先,则x本身就是两者的最近公共祖先

求法

通常来说有四种

  • 树上倍增

  • dfs序与ST表

  • 树链剖分

  • Tarjan (离线算法)

这里我们只讨论前三种在线算法

方法一:树上倍增

倍增的思想可以看一下这篇文章:算法学习笔记(3): 倍增与ST算法

这是基于朴素方法的优化,所以在讲树上倍增之前我们还需要讲一下朴素的方法


朴素算法

选择x和y中深度较大的点,向上跳直到两者深度相同,然后两者同时向上跳直到两者第一次相遇,此时两者所在即是最近公共祖先


显然,我们每次只跳一个显然不优,所以借鉴倍增与二进制拆分的思想

令fa[x][k]指从x开始,向上跳2^k次所能达到的祖先,特别的,如果深度不够,则fa[x][k] = 0

那么,基于朴素算法的流程:我们只需要跳的时候利用倍增的思想就行了

这里给出一种参考代码:

notice: 这其实不是一种特别好的写法

int lca(int x, int y) {
int p;
// 这里保证了x是深度相对较大那一个
if (dep[x] < dep[y]) p = x, x = y, y = p; // 将x跳到与y深度相同的位置
p = 0;
while (p >= 0) {
if (dep[fa[x][p]] >= dep[y]) x = fa[x][p], ++p;
else --p;
} // 达到同样深度就已经相遇(y是x的祖先)
if (x == y) return x; // 两者同时向上跳直到相遇
p = 0;
while (p >= 0) {
if (fa[x][p] != fa[y][p]) x = fa[x][p], y = fa[y][p], ++p;
else --p;
}
// 最后还要向上跳一个才是公共祖先
// 通过这种倍增写法得出的位置是最后一个满足if条件的位置!!!
return fa[x][0];
}

那么问题来了,如何初始化?

显而易见,跳2^k步相当于跳2次2^(k-1)步,所以有了

fa[x][i] = fa[fa[x][i - 1]][i - 1];

复杂度分析

在初始化的时候每一个点都需要O(logN),那么总初始化需要O(NlogN)

在询问时,令k为x, y到最近公共祖先的最大距离,根据倍增算法,则复杂度为O(logk),当树退化成一条链时,k最大取到n,那么最终复杂度应该为O(logN)

综上,初始化需要O(NlogN),单次询问需要O(logN)

方法二:dfs序与ST表

这种方法相对难以理解,所以读者可以在掌握了一般的Tarjan缩点算法后再理解(这两个算法几乎没有关系,但是一些思想在Tarjan中有所体现)

下文中不是特别明白名词在后文中有所提及,或者可以参考讲解Tarjan算法的文章

先求出树每一个结点的dfn,不妨令x比y更先遍历到(dfn[x] < dfn[y])

  • 若x不是y的祖先

在dfs序列中,区间[dfn[x], dfn[y]]内一定包含了LCA子树的部分,但不包含LCA及其祖先,并且该区间一定包含了LCA到y路径上的第一个儿子,而且它的深度是最小的

所以,我们只需要找到区间[dfn[x], dfn[y]]中深度最小的点,它的父结点就是LCA

  • 若x是y的祖先,则x就是LCA,且x是区间[dfn[x], dfn[y]]中深度最小的点,与上述情况不太一样,那么考虑我们将x排除在区间外,则在区间[dfn[x] + 1, dfn[y]]中深度最小的点的父亲结点就是x。返回考虑第一种情况,由于第一种情况x一定不是LCA,那么将区间替换为[dfn[x] + 1, dfn[y]]不影响答案。

综上,我们只需要求区间[dfn[x] + 1, dfn[y]] 中深度最小的点的父结点即可。

初始化与查询

定义rdfn满足x == rdfn[dfn[x]],ST表初始化时f[i][0] = rdfn[i]。

其实可以只需要dfn就ok,f[dfn[i]][0] = i

定义区间操作

int Min(int x, int y) {
return depth[x] < depth[y] ? x : y;
}

定义查询操作

int query(int x, int y) {
if (x == y) return x; // 同一个结点啊
// 确保x是先遍历到的那一个结点
if (dfn[x] > dfn[y]) std::swap(x, y);
int k = log(dfn[y] - dfn[x]);
return Min(f[dfn[x] + 1][k], f[dfn[y] - (1<<k) + 1][k]);
}

其余的就套模板就行了

复杂度分析

初始化其实主要是ST表的初始化,所以初始化的复杂度为O(NlogN)(实际上应该是O(N + NlogN)

查询也是ST表的区间查询,复杂度为O(1)

方法三:树链剖分

好高级的名字……

树链剖分指将一棵树分割为若干条链,以维护树上信息。它包含重链剖分、长链剖分、实链剖分,在没有特殊说明的情况下,树链剖分一般指代重链剖分。这里也着重描述重链剖分。

这里先给出一些

DFS序

dfs序指dfs的顺序。具体方法是用一个计数变量cnt,遍历到结点x时,时间戳dfn[x] = ++cnt,对应dfs序列中第cnt个就是x

而上文定义中的rdfn则可以在同时通过rdfn[cnt] = x记录下来

性质
  • 结点的dfn大于其任意父结点

  • 对于任意结点(包含本身)的子树中,存在一个dfn是连续的一条链

如果子树在dfs序列中是连续的一段,在维护子树(子树求和之类的子树操作)时,直接把它看做一个序列,那么就和在线段树上操作没有区别了

重链

相邻重边链接形成的链

重边

重子结点(如果有)与其父结点相连的边

重子结点

一个结点的子结点中度最大的结点(孩子个数最多的点)

剖分方法

重链剖分就是将每一条重链单独划分出来,从而通过dfn来维护链上信息

至于如何剖分,需要进行两次遍历

第一次遍历,获得所有节点的siz, dep, fa, son

这里siz指结点的度,三个字符统一一下,fa是个例外,dep指结点的深度,fa指结点的父结点,son指结点的重子结点

第二次遍历,获得所有点的dfn, top

这里dfn无需解释,top指结点所在重链最顶部的结点

思路清晰,这里给出一种参考写法

注意前提,点编号从1 ~ n,且这里用的是前向星存图(树)

int top[N], dfn[N], son[N], dep[N], siz[N], fa[N];
// work son, siz, dep and fa
void workSSDF(int x, int par) {
dep[x] = dep[par] + 1, fa[x] = par, siz[x] = 1;
for (int y, i = head[x]; i; i = edge[i].next) {
y = edge[i].to;
if (y == par) continue;
workSSDF(y, x);
siz[x] += siz[y];
if (siz[y] >= siz[son[x]]) son[x] = y;
}
} // work dfn and top
int cdfn = 0;
void workDT(int x, int ptop) {
dfn[x] = ++cdfn, top[x] = ptop;
if (son[x]) workDT(son[x], top[x]); for (int y, i = head[x]; i; i = edge[i].next) {
y = edge[i].to;
if (!dfn[y]) workDT(y, y);
}
}

剖分作用

这么麻烦的求这个树链剖分有啥用?

如果我们随便画一个图并模拟一下上述过程……

graph TD;
A[结点编号]
1---2;
1---3;
3---4;
3---5;
3---7;
4---8;
8---9;
5---6;

在两次遍历完之后:

graph LR;
A[结点上的是dfn序]
subgraph 重链1
1 === 2;
2 === 3;
3 === 4;
4 === 5;
end
subgraph 重链2
6 === 7;
end
subgraph 重链3
2 --- 8
end
subgraph 重链4
1 --- 9
end
2 --- 6

考虑用纯markdown画图是真的难受,但树的结构是一样的……T^T

我们很容易发现,在同一条重链上的结点的dfn序是连续的……(这我们稍后在论述)

回到求LCA的流程,由于我们把树分为了从上到下的一些链

注意每一条链中没有深度相同的结点

所以,我们只需要不断的把两个结点通过top向上跳直到在同一条链上(此时top[x] == top[y])

至于为什么要跳top结点的深度较大的结点……还请读者自行讨论

有一个需要注意的地方,不能直接x = top[x],因为top[top[x]] == top[x]!!!

所以还需要跳到top[x]的父节点上

流程清晰,这里给出一种参考代码

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

复杂度分析

太难写了 Q^Q

依据重链的定义,每一条重链至少占了重链顶端结点所在的子树结点的\(logN\)个结点(一条链就占满了)

所以,剖分之后差不多是有\(logN\)条重链,所以每次询问至多跳\(logN\)次,所以……

初始化复杂度为O(N)(两次dfs),查询复杂度为O(logN)

树链剖分拓展

看我的另一篇文章吧,太难写了,四千多个字呜啊

算法学习笔记(5): 最近公共祖先(LCA)的更多相关文章

  1. 算法学习笔记:最近公共祖先(LCA问题)

    当我们处理树上点与点关系的问题时(例如,最简单的,树上两点的距离),常常需要获知树上两点的最近公共祖先(Lowest Common Ancestor,LCA).如下图所示: 2号点是7号点和9号点的最 ...

  2. 算法详解之最近公共祖先(LCA)

    若图片出锅请转至here 概念 首先是最近公共祖先的概念(什么是最近公共祖先?): 在一棵没有环的树上,每个节点肯定有其父亲节点和祖先节点,而最近公共祖先,就是两个节点在这棵树上深度最大的公共的祖先节 ...

  3. Luogu 2245 星际导航(最小生成树,最近公共祖先LCA,并查集)

    Luogu 2245 星际导航(最小生成树,最近公共祖先LCA,并查集) Description sideman做好了回到Gliese 星球的硬件准备,但是sideman的导航系统还没有完全设计好.为 ...

  4. POJ 1330 Nearest Common Ancestors / UVALive 2525 Nearest Common Ancestors (最近公共祖先LCA)

    POJ 1330 Nearest Common Ancestors / UVALive 2525 Nearest Common Ancestors (最近公共祖先LCA) Description A ...

  5. [模板] 最近公共祖先/lca

    简介 最近公共祖先 \(lca(a,b)\) 指的是a到根的路径和b到n的路径的深度最大的公共点. 定理. 以 \(r\) 为根的树上的路径 \((a,b) = (r,a) + (r,b) - 2 * ...

  6. 【lhyaaa】最近公共祖先LCA——倍增!!!

    高级的算法——倍增!!! 根据LCA的定义,我们可以知道假如有两个节点x和y,则LCA(x,y)是 x 到根的路 径与 y 到根的路径的交汇点,同时也是 x 和 y 之间所有路径中深度最小的节 点,所 ...

  7. C / C++算法学习笔记(8)-SHELL排序

    原始地址:C / C++算法学习笔记(8)-SHELL排序 基本思想 先取一个小于n的整数d1作为第一个增量(gap),把文件的全部记录分成d1个组.所有距离为dl的倍数的记录放在同一个组中.先在各组 ...

  8. POJ 1470 Closest Common Ancestors(最近公共祖先 LCA)

    POJ 1470 Closest Common Ancestors(最近公共祖先 LCA) Description Write a program that takes as input a root ...

  9. Manacher算法学习笔记 | LeetCode#5

    Manacher算法学习笔记 DECLARATION 引用来源:https://www.cnblogs.com/grandyang/p/4475985.html CONTENT 用途:寻找一个字符串的 ...

  10. Johnson算法学习笔记

    \(Johnson\)算法学习笔记. 在最短路的学习中,我们曾学习了三种最短路的算法,\(Bellman-Ford\)算法及其队列优化\(SPFA\)算法,\(Dijkstra\)算法.这些算法可以快 ...

随机推荐

  1. Vue中组件化编码 完成任务的添加、删除、统计、勾选需求(实战练习三完结)

    上一个章节实现数据在组件之间的传递 .这一章主要是完成添加任务到任务栏.删除任务栏.统计任务完成情况.主要还是参数在各个组件之间的传递. 上一章节的链接地址:https://blog.csdn.net ...

  2. Go | 讲解GOROOT、GOPATH、GOBIN

    前言 Go(又称 Golang)是 Google 开发的一种静态强类型.编译型.并发型,并具有垃圾回收功能的编程语言.Go 被誉为是未来的服务器端编程语言. Go是一门全新的静态类型开发语言,具有自动 ...

  3. NLP之基于Transformer的句子翻译

    Transformer 目录 Transformer 1.理论 1.1 Model Structure 1.2 Multi-Head Attention & Scaled Dot-Produc ...

  4. iOS开发之自定义日历控件

    前言 日常开发中经常会遇到日期选择,为了方便使用,简单封装了一个日历控件,在此抛砖引玉供大家参考. 效果 功能 支持单选.区间 支持默认选中日期 支持限制月份 支持过去.当前.未来模式 支持frame ...

  5. vue2和vue3组合使用教程地址

    https://cn.vuejs.org/guide/essentials/watchers.html#eager-watchers

  6. Nginx 使用自签名证书实现 https 反代 Spring Boot 中碰到的页面跳转问题

    问题一:页面自动跳转到 80 端口 问题描述 最近在使用Nginx反代一个Spring Boot项目中碰到了一个问题,使用 Spring Boot 中的 redirect: 进行页面跳转的时候,通过 ...

  7. android 代码分析

    1.@Override注解 @Override 注解是用来指定方法重写的,只能修饰方法并且只能用于方法重写,不能修饰其它的元素. 作用是告诉编译器检查这个方法,保证父类要包含一个被该方法重写的方法,否 ...

  8. 某厂面试:如何优雅使用 SPI 机制

    代码不多,文章可能有点长.朋友面试某厂问到的 SPI 机制,联想到自己项目最近写到的 SPI 场景,文章简要描述下 SPI 机制的发展历程 产出背景 因为最近项目中使用分库分表以及数据加密使用到了 S ...

  9. Java-数组工具类Arrays

    java.util.Arrays是一个与数组相关的工具类,里面提供了大量静态方法,用来实现数组常见的操作. toSting方法 public static String toString(数组):将参 ...

  10. win 10 玩红警/黑边,不能全屏,闪退

    win 10玩红警黑边问题 1.下载ddraw.dll,放在游戏目录 下载链接:ddraw.dll 如果提示 选择保留就行了 2.Win 键+S键,搜索注册表,打开这个 进去按这个路径    计算机\ ...