最近公共祖先LCA问题
来源:互联网 发布:淘宝的一元拍卖代理 编辑:程序博客网 时间:2024/05/16 08:59
本文出处:https://github.com/julycoding/The-Art-Of-Programming-By-July/blob/master/ebook/zh/03.03.md
解法一:暴力对待
1.1、二叉查找树
在当这棵树是二叉查找树的情况下。那么从树根开始:
- 如果当前结点t 大于结点u、v,说明u、v都在t 的左侧,所以它们的共同祖先必定在t 的左子树中,故从t 的左子树中继续查找;
- 如果当前结点t 小于结点u、v,说明u、v都在t 的右侧,所以它们的共同祖先必定在t 的右子树中,故从t 的右子树中继续查找;
- 如果当前结点t 满足 u <t < v,说明u和v分居在t 的两侧,故当前结点t 即为最近公共祖先;
- 而如果u是v的祖先,那么返回u的父结点,同理,如果v是u的祖先,那么返回v的父结点。
//copyright@eriol 2011 //modified by July 2014 public int query(Node t, Node u, Node v) { int left = u.value; int right = v.value; Node parent = null; //二叉查找树内,如果左结点大于右结点,不对,交换 if (left > right) { int temp = left; left = right; right = temp; } while (true) { //如果t小于u、v,往t的右子树中查找 if (t.value < left) { parent = t; t = t.right; //如果t大于u、v,往t的左子树中查找 } else if (t.value > right) { parent = t; t = t.left; } else if (t.value == left || t.value == right) { return parent.value; } else { return t.value; } } }
1.2、不是二叉查找树
但如果这棵树不是二叉查找树,只是一棵普通的二叉树呢?如果每个结点都有一个指针指向它的父结点,于是我们可以从任何一个结点出发,得到一个到达树根结点的单向链表。因此这个问题转换为两个单向链表的第一个公共结点。
此外,如果给出根节点,LCA问题可以用递归很快解决。而关于树的问题一般都可以转换为递归(因为树本来就是递归描述),参考代码如下:
//copyright@allantop 2014-1-22-20:01 node* getLCA(node* root, node* node1, node* node2) { if(root == null) return null; if(root== node1 || root==node2) return root; node* left = getLCA(root->left, node1, node2); node* right = getLCA(root->right, node1, node2); if(left != null && right != null) return root; else if(left != null) return left; else if (right != null) return right; else return null; }
解法二:转换为RMQ问题
解决此最近公共祖先问题的还有一个算法,即转换为RMQ问题,用Sparse Table(简称ST)算法解决。
2.1、什么是RMQ问题
RMQ,全称为Range Minimum Query,顾名思义,则是区间最值查询,它被用来在数组中查找两个指定索引中最小值的位置。即RMQ相当于给定数组A[0, N-1],找出给定的两个索引如 i、j 间的最小值的位置。
假设一个算法预处理时间为 f(n),查询时间为g(n),那么这个算法复杂度的标记为。我们将用RMQA(i, j) 来表示数组A 中索引i 和 j 之间最小值的位置。 u和v的离树T根结点最远的公共祖先用LCA T(u, v)表示。
如下图所示,RMQA(2,7 )则表示求数组A中从A[2]~A[7]这段区间中的最小值:
很显然,从上图中,我们可以看出最小值是A[3] = 1,所以也就不难得出最小值的索引值RMQA(2,7) = 3。
2.2、如何解决RMQ问题
2.2.1、Trivial algorithms for RMQ
下面,我们对对每一对索引(i, j),将数组中索引i 和 j 之间最小值的位置 RMQA(i, j) 存储在M[0, N-1][0, N-1]表中。 RMQA(i, j) 有不同种计算方法,你会看到,随着计算方法的不同,它的时空复杂度也不同:
- 普通的计算将得到一个 复杂度的算法。尽管如此,通过使用一个简单的动态规划方法,我们可以将复杂度降低到。如何做到的呢?方法如下代码所示:
/copyright@ //modified by July 2014 void process1(int M[MAXN][MAXN], int A[MAXN], int N) { int i, j; for (i =0; i < N; i++) M[i][i] = i; for (i = 0; i < N; i++) for (j = i + 1; j < N; j++) //若前者小于后者,则把后者的索引值付给M[i][j] if (A[M[i][j - 1]] < A[j]) M[i][j] = M[i][j - 1]; //否则前者的索引值付给M[i][j] else M[i][j] = j; }
- 一个比较有趣的点子是把向量分割成sqrt(N)大小的段。我们将在M[0,sqrt(N)-1]为每一个段保存最小值的位置。如此,M可以很容易的在O(N)时间内预处理。
- 一个更好的方法预处理RMQ 是对2^k 的长度的子数组进行动态规划。我们将使用数组M[0, N-1][0, logN]进行保存,其中M[ i ][ j ] 是以i 开始,长度为 2^j 的子数组的最小值的索引。这就引出了咱们接下来要介绍的Sparse Table (ST) algorithm。
3.2.2、Sparse Table (ST) algorithm
在上图中,我们可以看出:
- 在A[1]这个长度为2^0的区间内,最小值即为A[1] = 4,故最小值的索引M[1][0]为1;
- 在A[1]、A[2] 这个长度为2^1的区间内,最小值为A[2] = 3,故最小值的索引为M[1][1] = 2;
- 在A[1]、A[2]、A[3]、A[4]这个长度为2^2的区间内,最小值为A[3] = 1,故最小值的索引M[1][2] = 3。
为了计算M[i][j]我们必须找到前半段区间和后半段区间的最小值。很明显小的片段有着2^(j-1)长度,因此递归如下
(上述递推公式有误,应该是M[ i, j] = min( M [ i, j - 1], M [ i + 2 ^ (j - 1), j - 1]).)
根据上述公式,可以写出这个预处理的递归代码,如下:
void process2(int M[MAXN][LOGMAXN], int A[MAXN], int N) { int i, j; //initialize M for the intervals with length 1 for (i = 0; i < N; i++) M[i][0] = i; //compute values from smaller to bigger intervals for (j = 1; 1 << j <= N; j++) for (i = 0; i + (1 << j) - 1 < N; i++) if (A[M[i][j - 1]] < A[M[i + (1 << (j - 1))][j - 1]]) M[i][j] = M[i][j - 1]; else M[i][j] = M[i + (1 << (j - 1))][j - 1]; }
经过这个O(N logN)时间复杂度的预处理之后,让我们看看怎样使用它们去计算 RMQA(i, j)。思路是选择两个能够完全覆盖区间[i..j]的块并且找到它们之间的最小值。设k = [log(j - i + 1)]。
为了计算 RMQA(i, j),我们可以使用下面的公式:
故,综合来看,咱们预处理的时间复杂度从O(N3)降低到了O(N logN),查询的时间复杂度为O(1),所以最终的整体复杂度为:< O(N logN), O(1) >。
2.3、LCA与RMQ的关联性
现在,让我们看看怎样用RMQ来计算LCA查询。事实上,我们可以在线性时间里将LCA问题规约到RMQ问题,因此每一个解决RMQ的问题都可以解决LCA问题。让我们通过例子来说明怎么规约的:
注意LCAT(u, v)是在对T进行dfs过程当中在访问u和v之间离根结点最近的点。因此我们可以考虑树的欧拉环游过程u和v之间所有的结点,并找到它们之间处于最低层的结点。为了达到这个目的,我们可以建立三个数组:
- E[1, 2*N-1] - 对T进行欧拉环游过程中所有访问到的结点;E[i]是在环游过程中第i个访问的结点
- L[1,2*N-1] - 欧拉环游中访问到的结点所处的层数;L[i]是E[i]所在的层数
- H[1, N] - H[i] 是E中结点i第一次出现的下标(任何出现i的地方都行,当然选第一个不会错)
假定H[u]<Hv。可以很容易的看到u和v第一次出现的结点是E[H[u]..H[v]]。现在,我们需要找到这些结点中的最低层。为了达到这个目的,我们可以使用RMQ。因此 LCAT(u, v) = E[RMQL(H[u], H[v])] ,RMQ返回的是索引,下面是E,L,H数组:
注意L中连续的元素相差为1。
2.4、从RMQ到LCA
我们已经看到了LCA问题可以在线性时间规约到RMQ问题。现在让我们来看看怎样把RMQ问题规约到LCA。这个意味着我们实际上可以把一般的RMQ问题规约到带约束的RMQ问题(这里相邻的元素相差1)。为了达到这个目的,我们需要使用笛卡尔树。
对于数组A[0,N-1]的笛卡尔树C(A)是一个二叉树,根节点是A的最小元素,假设i为A数组中最小元素的位置。当i>0时,这个笛卡尔树的左子结点是A[0,i-1]构成的笛卡尔树,其他情况没有左子结点。右结点类似的用A[i+1,N-1]定义。注意对于具有相同元素的数组A,笛卡尔树并不唯一。在本文中,将会使用第一次出现的最小值,因此笛卡尔树看作唯一。可以很容易的看到RMQA(i, j) = LCAC(i, j)。
下面是一个例子:
现在我们需要做的仅仅是用线性时间计算C(A)。这个可以使用栈来实现。
- 初始栈为空。
- 然后我们在栈中插入A的元素。
- 在第i步,A[i]将会紧挨着栈中比A[i]小或者相等的元素插入,并且所有较大的元素将会被移除。
- 在插入结束之前栈中A[i]位置前的元素将成为i的左儿子,A[i]将会成为它之后一个较小元素的右儿子。
在每一步中,栈中的第一个元素总是笛卡尔树的根。
如果使用栈来保存元素的索引而不是值,我们可以很轻松的建立树。由于A中的每个元素最多被增加一次和最多被移除一次,所以建树的时间复杂度为O(N)。最终查询的时间复杂度为O(1),故综上可得,咱们整个问题的最终时间复杂度为:。
现在,对于询问 RMQA(i, j) 我们有两种情况:
- i和j在同一个块中,因此我们使用在P和T中计算的值
- i和j在不同的块中,因此我们计算三个值:从i到i所在块的末尾的P和T中的最小值,所有i和j中块中的通过与处理得到的最小值以及从j所在块i和j在同一个块中,因此我们使用在P和T中计算的值j的P和T的最小值;最后我们我们只要计算三个值中最小值的位置即可。
RMQ和LCA是密切相关的问题,因为它们之间可以相互规约。有许多算法可以用来解决它们,并且他们适应于一类问题。
- 最近公共祖先--LCA问题
- LCA(最近公共祖先)问题
- 最近公共祖先LCA问题
- 最近公共祖先LCA问题
- 最近公共祖先LCA问题
- LCA(最近公共祖先)问题
- 最近公共祖先问题LCA
- LCA 最近公共祖先问题
- 最近公共祖先LCA
- 最近公共祖先(LCA)
- Lca 最近公共祖先
- LCA----最近公共祖先
- LCA (最近公共祖先)
- LCA最近公共祖先
- LCA 最近公共祖先
- 最近公共祖先 LCA
- LCA--最近公共祖先
- LCA(最近公共祖先)
- ASP获取新增记录ID值的方法
- 和往常一样,妻悄悄走了进来
- 正则替换换行符和把 br 替换成换行符
- 我一笑:怎么突然有这样的兴趣呢,前两年
- thinkphp3.2.x版本中图片上传缩略图的解决方案
- 最近公共祖先LCA问题
- ThinkPHP多表联合查询的常用方法
- udp学习笔记
- 黑马程序员--入学考试第2题
- SpringMVC入门
- cocos2dx 中 Android NDK 加载动态库的问题
- 最常见的的优化手法有哪些?汇总常见的SEO快速优化方式
- 黑马程序员--入学考试第3题
- redis内部数据结构之字典