最近公共祖先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是密切相关的问题,因为它们之间可以相互规约。有许多算法可以用来解决它们,并且他们适应于一类问题。


0 0
原创粉丝点击