一、最近公共祖先(LeastCommon Ancestors)
对于有根树T的两个结点u、v,最近公共祖先LCA(T,u,v)表示一个结点x,满足x是u、v的祖先且x的深度尽可能大。另一种理解方式是把T理解为一个无向无环图,而LCA(T,u,v)即u到v的最短路上深度最小的点。
这里给出一个LCA的例子:
例一
对于T=<V,E>
V={1,2,3,4,5}
E={(1,2),(1,3),(3,4),(3,5)}
则有:
LCA(T,5,2)=1
LCA(T,3,4)=3
LCA(T,4,5)=3
二、RMQ问题(Range Minimum Query)
RMQ问题是指:对于长度为n的数列A,回答若干询问RMQ(A,i,j)(i,j<=n),返回数列A中下标在[i,j]里的最小值下标。这时一个RMQ问题的例子:
例二
对数列:5,8,1,3,6,4,9,5,7有:
RMQ(2,4)=3
RMQ(6,9)=6
RMQ问题与LCA问题的关系紧密,可以相互转换,相应的求解算法也有异曲同工之妙。下面给出LCA问题向RMQ问题的转化方法。
对树进行深度优先遍历,每当“进入”或回溯到某个结点时,将这个结点的深度存入数组E最后一位。同时记录结点i在数组中第一次出现的位置(事实上就是进入结点i时记录的位置),记做R[i]。如果结点E[i]的深度记做D[i],易见,这时求LCA(T,u,v),就等价于求E[RMQ(D,R[u],R [v])],(R[u]<R[v])。例如,对于第一节的例一,求解步骤如下:
数列E[i]为:1,2,1,3,4,3,5,3,1
R[i]为:1,2,4,5,7
D[i]为:0,1,0,1,2,1,2,1,0
于是有:
LCA(T,5,2) = E[RMQ(D,R[2],R[5])] =E[RMQ(D,2,7)] = E[3] = 1
LCA(T,3,4) = E[RMQ(D,R[3],R[4])] =E[RMQ(D,4,5)] = E[4] = 3
LCA(T,4,5) = E[RMQ(D,R[4],R[5])] =E[RMQ(D,5,7)] = E[6] = 3
易知,转化后得到的数列长度为树的结点数的两倍加一,所以转化后的RMQ问题与LCA问题的规模同次。
三、笛卡儿树(CartesianTree)
笛卡儿树是一种特殊的堆,它的重要应用之一是实现RMQ问题向LCA问题的转化。笛卡儿树根据一个数列构造,其根结点是长为n的数列A中的最小值A[i] 的下标i,左右孩子分别是由数列A[1...i-1]和A[i+1...n]构造的笛卡儿树。下面的内容里,我们说笛卡儿树某结点的值,指的是其存储的下标在原数组里对应的值。
由于笛卡儿树的这一特性,不难发现求原数列A的某一段最小值,相当于求这一段的左右两端在笛卡儿树上所对应结点的最近公共祖先。
这里有一个定理:
数组A的Cartesian树记为C(A),则RMQ(A,i,j)=LCA(C(A),i,j).证明略。
现在急待解决的问题是笛卡儿树的建立方法。笛卡儿树不一定是完全二叉树,所以与堆的操作有些不同。由于笛卡儿树的特性,对其进行中序遍历一定会得到原数列,也就是说,可以把笛卡儿树看作是将原数列中的最小值“提升”到最高处,再将左右两部分的最小值分别“提升”到次高处…,最终形成一棵二叉树。所以我们由A[1]开始逐个将数列里的数加入笛卡儿树内,新加入的结点一定在树的最右路径的最右端(没有右孩子)。当加入A[i]时我们在已有的笛卡儿树上找到最右路径上找到大于A[i]最小值,将它的父结点作为新结点的父结点,它则作为新结点的左孩子。若根结点大于A[i]则整个树作为新结点的左孩子,若最右路径上没有大于A[i]的结点,则A[i]加入最右路径的最右下端。
由于每个结点最多进入和退出最右路径各一次,因此均摊时间复杂度为θ(n)。
四、RMQ问题的稀疏表算法(Sparse Table)
RMQ的最优子结构比较明显,很容易得到递推式:RMQ(i,j)=min{RMQ(i,k),RMQ(k,j)},i<k<j。为了更有效地计算所有状态,我们尽可能平分计算时j-i的长度,运算规模的递规方程为T(n)=2T(n/2)+1,易知算法的时间复杂度为θ(n*lg(n))。
值的注意的是,由LCA问题转化而来的RMQ问题,数列中相邻两项的差总是1,这种特殊情况被称作±1RMQ问题,由于这样的长度为n的不同的数列只有2^n个,算法可以进一步优化为θ(n)。这种方法可以实现了LCA和RMQ问题的θ(n)-θ(1)的在线算法。由于±1RMQ的θ(n)-θ (1)算法时间比较繁琐(个人见解),而且不能在同样的复杂度下解决一般RMQ(数列相邻项的差可能大于1)问题,本文将着重讲通过将一般RMQ问题转换为LCA问题的求解方法。
Ne[Rwa]告诉我,他通过分段表实现了一种简便的一般RMQ问题的O(n)-O(n^0.5)的算法。相比ST算法,这个算法在空间上似乎更优。以下是他本人对这种算法的介绍:
先把待求RMQ的数组分成每段长度lenth=sqrt(n)的小段,用数组A记录每段的最小值,这样先判断i和j所在的块x,y如果x=y,那么RMQ (L,i,j)=IN_RMQ(x,i,j)否则:RMQ(L,i,j)=min{In_RMQ(x,i,lenth),IN_RMQ(y,1,j),RMQ(A,x+1,y-1)}就可以在很快的时间内求出RMQ问题了。预处理的时间复杂度为O(n),每次RMQ最坏情况的时间复杂度为O (n^0.5)。
例如:
L={3,9,7,6,5,2,1,4,8,6,6,4,8,7,9,2}则分成4段A={3,1,4,2}
要求RMQ(L,3,15)
那么IN_RMQ(L,3,4)=6,RMQ(A,2,3)=1,IN_RMQ(L,13,15)=7
所以数组里最小的元素为1,下标为7。
五、LCA问题的Tarjan离线算法
利用并查集优越的时空复杂度,我们可以实现LCA问题的O(n+Q)算法,这里Q表示询问的次数。Tarjan算法基于深度优先搜索的框架,对于新搜索到的一个结点,首先创建由这个结点构成的集合,再对当前结点的每一个子树进行搜索,每搜索完一棵子树,则可确定子树内的LCA询问都已解决。其他的LCA询问的结果必然在这个子树之外,这时把子树所形成的集合与当前结点的集合合并,并将当前结点设为这个集合的祖先。之后继续搜索下一棵子树,直到当前结点的所有子树搜索完。这时把当前结点也设为已被检查过的,同时可以处理有关当前结点的LCA询问,如果有一个从当前结点到结点v的询问,且v已被检查过,则由于进行的是深度优先搜索,当前结点与v的最近公共祖先一定还没有被检查,而这个最近公共祖先的包涵v的子树一定已经搜索过了,那么这个最近公共祖先一定是v 所在集合的祖先。
Tarjan和LCA转化为RMQ的代码(对应的题目是BOJ的p1278):
最近公共祖先
Submit: 326 Accepted:111
Time Limit: 1000MS Memory Limit:65536K
Description
给定一棵树,我们定义树中两个节点A、B的最近公共祖先C为:
1、C既在A到根的路径上,又在B到根的路径上。
2、C离根的距离最远。
如下图:
节点4、5的最近公共祖先是8;节点9、6的最近公共祖先是8;节点7、11的最近公共祖先是4。
Input
输入包含多组测试数据。
首先第一行输入一个数T(T<=20),表示总共有T组测试数据。
接下来是每组测试数据,第一行是一个数N(N<=200),表示树中一共有N个点。接下来N-1行,每行两个数A、B,表示A是B的父亲。
然后是一个数M(M<=1000),表示一共有M个查询,接下来每个查询有两个数A、B,表示询问A和B的最近公共祖先。
Output
首先,输出Case #X:其中X代表是第X组数据(具体格式参照样例)。
然后对每次查询,输出A和B的最近公共祖先。
Sample Input
2
2
1 2
1
1 2
3
1 2
1 3
2
1 2
2 3
Sample Output
Case #1:
1
Case #2:
1
1
Source
humanjustic
#include <cstdio>
#include <cstring>
#include <iostream>
#include <cmath>
#define size 10001
#define msize 16
using namespace std;
int tree[size] ; //tree[a]保存着a的父亲
int ord[2*size+1][2];//记录着节点深度优先遍历的顺序和深度(根节点的深度为0)
int num; //ord数组的元素个数
int n ; //节点的数量
int Rnum[2*size+1][msize];
int First[size];
void change(int k , int de, int & nu)//把LCA转化为RMQ
{
ord[nu][0]=k;
ord[nu][1]=de;
First[k]=nu;
for (int i=1 ; i<=n ; i++)
if (tree[i]==k)
{
++nu;
change(i,de+1,nu);
ord[++nu][0]=k;
ord[nu][1]=de;
}
}
void RMQ_Predo( int k)
{
for ( int i=0 ; i<k ; i++ ) Rnum[i][0]=i;
for ( int j=1; (1 << j) <=k ; j++ )
for (int i=1 ; i+(1 <<j)-1 <k ; i++)
if (ord[Rnum[i][j-1]][1] < ord[Rnum[i+(1 << (j-1))][j-1]][1] )Rnum[i][j]=Rnum[i][j-1];
else Rnum[i][j]=Rnum[i+(1 << ( j-1 ))][j-1];
}
int get_ans(int i, int j)
{
int len=(int)(log((float)(j-i+1)/(log(2.0)))/(log(2.0)));
if (ord[Rnum[i][len]][1] > ord[Rnum[j-(1 << len)+1][len]][1])
return ord[Rnum[j- (1 << len ) +1][len]][0];
else return ord[Rnum[i][len]][0];
}
int get_root( int k) //找出树的根
{
for ( int i=1 ; i<=k ; i++ )
if (! tree[i]) return i;
}
int main ( )
{
int a,b;
scanf("%d", &n);
for ( int i=1 ; i< n ; i++ )
{
scanf("%d%d",&a,&b);
tree[b]=a;
}
num=0;
change(get_root(n),0,num);
RMQ_Predo(num);
int m;
scanf("%d",&m);
for ( int i=1 ; i<=m ; i ++ )
{
scanf("%d%d",&a,&b);
int x=First[a],y=First[b] ;
if ( x < y ) printf("%d",get_ans(x,y));
else printf("%d",get_ans(y,x));
}
}