- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
最近公共祖先是树上的概念,不了解树的出门左转百度: 树(数据结构名词)_百度百科 。
假设我们需要求 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) 。
这种方法相对难以理解,所以读者可以在掌握了一般的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的顺序。具体方法是用一个计数变量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);
}
}
这么麻烦的求这个树链剖分有啥用?
如果我们随便画一个图并模拟一下上述过程…… 。
在两次遍历完之后:
考虑用纯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)的文章就讲到这里了,如果你想了解更多关于算法学习笔记(5):最近公共祖先(LCA)的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我正在尝试构建一个页面大小三分之一的容器,但我收到错误 No MediaQuery ancestor could be found starting from the context that was
给定包含数据的表T_Person(姓名,父级) +--------+--------+| name | parent |+--------+--------+| john | peter |
假设输入 XML 为 Test Me 我想找到 title 和 author 的最低共同祖先。我在 BaseX 中尝试了以下代码: let $p := doc('t.xq
给定表 T_Person(姓名, parent ) 包含数据 +--------+--------+| name | parent |+--------+--------+| john | p
我在leetcode中找到了java中最低公共(public)祖先问题的解决方案。换句话说,问题是找到 p 和 q 的最低共同祖先,并且 BST 的根为根。这是我的代码。 public TreeNod
我知道如果这两个节点必须在二叉树中如何解决问题,但是如果它们不必在树中怎么办?如果树中只有一个或没有这些节点,则返回 None。 这是我的代码: # Definition for a binary t
我在 stackoverflow 上查看了很多其他答案,但找不到任何有效的东西,我要么得到根,要么返回 node1 本身,我不知道如何递归地执行此操作,并且已经尝试了很多次一切都以同样的方式结束。任何
如果所有元素都不同,则很容易在 BST 中找到最近的共同祖先。但是,如果某些值相同怎么办。到目前为止,我们只是比较节点的数据,仅此而已,但现在我们是否需要检查节点的地址而不仅仅是值? 最佳答案 是的,
问题:给定一个二叉搜索树 (BST),找到 BST 中两个给定节点的最低公共(public)祖先 (LCA)。 根据维基百科上 LCA 的定义:“最低公共(public)祖先定义在两个节点 v 和 w
我正在尝试通过自上而下的递归来解决二叉树的最低公共(public)祖先(LCA)问题。 我使用的方法是: IDEA: Find the node which has one of the desire
我被这个问题绊倒了。以下是我的做法: Lets say the two nodes are node1 and node2 For any node (lets say node1), find th
关闭。这个问题需要debugging details .它目前不接受答案。 编辑问题以包含 desired behavior, a specific problem or error, and th
只是想知道以下算法在二叉搜索树中查找两个节点的最低公共(public)祖先的效率如何。 Node getLowestCommonAncestor (Node root, Node a, Node b)
如果我知道树中每个节点的邻接列表,那么如何找出该树中存在的任意两个节点的最低公共(public)祖先? 其实我想找出任意两个节点之间的距离,所以我想计算 LCA。有没有办法从邻接表中计算出来? T 中
这里的二叉树不一定是二叉搜索树。 该结构可以视为 - struct node { int data; struct node *left; struct node *right
以下是我对二叉搜索树的最低公共(public)祖先的实现。我有两个问题: 1) 时间复杂度为 O(n),空间复杂度为 O(n) 最坏情况,但如果 BST 平衡,时间和空间的平均情况为 O(logn)。
这是一个很受欢迎的面试问题,我能找到的关于该主题的唯一一篇文章来自 TopCoder .不幸的是,从面试答案的角度来看,它看起来过于复杂。 除了绘制到两个节点的路径并推导祖先之外,是否有更简单的方法来
我在面试中被问到这个问题,虽然我知道如何解决这个问题,但我没有回答。我不知道该怎么做的原因是因为问题是这样给我的: class Node{ int val; Node *lef
我正在尝试提出获取上下文项的所有祖先的解决方案。一种选择是将 _path 存储在索引中,另一种选择是执行类似于以下操作的操作: http://www.glass.lu/Blog/GettingAnce
我正在尝试用java实现n叉树中多个节点的LCA。我正在处理句子的解析树,因此假设节点的子节点数 <= 6 是合理的。这里的多个节点是句子中的两个短语(连续单词序列)。令 k 为涉及的节点数。 一种方
我是一名优秀的程序员,十分优秀!