两个月前有一次一个电话面试问到了一个问题:“怎样求二叉树中距离两个叶子节点最近的祖先节点。”当时不会,后来在网上查了查发现是一个比较经典的题目,也有几种算法可以解决这个问题,我学习了一下,在这儿记下来。这个问题更宽泛的定义是:如何求树(不限于二叉树)中两个节点(不限于叶子节点)的最近公共祖先节点。这个问题被称为LCA(Lowest Common Ancestor)问题。
我找到的关于这个问题解答的最全面的文章是这篇:二叉树中两个节点的最近公共祖先
按照文中的说法,解决这个问题有两个思路,一个为离线算法,另一个为在线算法。我查了一下这两组名词的含义,“在计算机科学中,一个在线算法是指它可以以序列化的方式一个个的处理输入,也就是说在开始时并不需要已经知道所有的输入。相对的,对于一个离线算法,在开始时就需要知道问题的所有输入数据,而且在解决一个问题后就要立即输出结果。例如,选择排序在排序前就需要知道所有待排序元素,然而插入排序就不必。”
此问题的离线和在线算法都涉及到了深度优先搜寻,在线算法引出了另外一个问题,称为RMQ(Range Minimum/Maximum Query)问题,而离线算法是一个专门的由一个外国人发明的Tarjan算法。
在查找到的资料中,LCA与RMQ是可以相互转化的,LCA向RMQ的转化是在在线算法的思路下进行的。下面说明一下他们之间的关系,这其中参考了一个PPT。
问题的提出:
LCA:基于有根树最近公共祖先问题。 LCA(T, u, v):在有根树T中,询问一个距离根最远的结点x,使得x同时为结点u、v的祖先。
RMQ:区间最小值询问问题。 RMQ(A, i, j):对于线性序列A中,询问区间[i, j]上的最小(最大)值。
RMQ向LCA的转化:
考察一个长度为N的序列A,按照如下方法将其递归建立为一棵树:
1)设序列中最小值为Ak,建立优先级为Ak的根节点Tk;
2)将A(1...k-1)递归建树作为Tk的左子树;
3)将A(k+1...N)递归建树作为Tk的右子树;
如序列A=(7, 5, 8, 1, 10)建树的结果为:
对于RMQ(A, i, j):
1)设序列中的最小值为Ak,若i<=k<=j,那么答案为k;
2)若k>j,那么答案为RMQ(A1...k-1, i, j);
3)若k
不难发现RMQ(A, i, j)=LCA(T, i, j),这就证明了RMQ问题可以转化为LCA问题。
LCA向RMQ问题的转化:
对有根树T进行DFS,将遍历到的节点按顺序记录下来,我们将得到一个长度为2N-1的序列,称之为T的欧拉序列F。每个节点都在欧拉序列中出现,我们记录节点u在欧拉序列中第一次出现的位置为pos(u)。如下树:
根据DFS的性质,对于两个节点u,v,从pos(u)遍历到pos(v)的过程中经过LCA(u, v)有且仅有一次,且深度是深度序列B[pos(u)...pos(v)]中最小的,即LCA(T, u, v)=RMQ(F, pos(u), pos(v)),并且问题的规模仍然是O(N)的。这就证明了LCA问题可以转化为RMQ问题。
RMQ问题:
RMQ问题是求给定区间中的最值问题,如下图所示:
假设一个算法的预处理时间为f(n),查询时间为g(n),则这个算法的时间复杂度标记为:
当然,最简单的算法是O(n)的,但是对于查询次数很多m(假设有100万次),则这个算法的时间复杂度为O(mn),显然时间效率太低。可以用线段树将查询算法优化到O(logn),而线段树的预处理时间复杂度为O(n),线段树整体复杂度为
(j=0,1...logn)的区间的最小值,然后在查询时将任何一个区间A[i..j]划分为两个预处理好的可能重叠的区间,取这两个重叠区间的最小值。下面把ST算法分为预处理和查询两部分。
1.预处理
预处理使用DP思想,用f(i, j)表示[i, i+2^j-1]区间中的最小值,即f(i, j)表示从第i个数起连续2^j个数中的最小值(例如,f(1, 0)表示[1, 1]中的最小值,就是num[1]),而任意一个长度为2^j的区间都可以划分为两个长度为2^(j-1)的区间,其中第一个区间的范围为:i...i+2^(j-1)-1,第二个区间范围为:i+2^(j-1)...i+2^j-1。所以f(i, j)可以由f(i, j-1)和f(i+2^(j-1), j-1)导出,而递推的初值(所有的f(i, 0)=num[i])都是已知的,所以我们可以采用自底向上的方法递推地给出所有符合条件的f(i, j)的值。ST的状态转移方程为:
2.查询
假设要查询从m到n这一段的最小值,m到n的区间长度为n-m+1,求出一个最大的k,使得k满足2^k<=(n-m+1),那么这个区间[m, n]可以被两个部分重叠的长度为2^k的区间完全覆盖,这两个区间为[m, m+2^k-1]和[n-2^k+1, n]。而我们之前已经求出了f(m, k)为[m, m+2^k-1]的最小值,f(n-2^k+1, k)为[n-2^k+1, n]的最小值。我们只要返回其中更小的那个,就是答案,算法时间复杂度为O(1)。
参考了一篇博客:RMQ问题的ST算法
ST算法在数组上的实现如下:
// Using ST(Sparse Table) to solve RMQ Problem
// M[i][j] stands for the subscript of the minimum number in the array range A[i,i+2^j-1]
#include
#include
#define K 100
int M[K][K];
//
int RMQinit(int a[], int len)
{
int i, j;
//int len=sizeof(a)/sizeof(int); 犯错误了
//printf("%d\n",len);
int lenJ=sqrt(len);
//int lenJ=log(len)/log(2);
// Initilization
for(i=0;i
// 二叉树LCA在线算法, 转化为RMQ问题
// c/c++是大小写敏感的, NULL=0, null只是一个符号
#include
#include
#define MAX 10
using namespace std;
int dfs[2*MAX];
int depth[2*MAX];
int pos[MAX]; // first apparence position
int M[2*MAX][2*MAX];
int len=0; // denote the subscript of the array
struct Node
{
int value;
Node *left;
Node *right;
Node(int val, Node *l, Node *r)
{
value=val;
left=l;
right=r;
}
};
// Depth First Search
int DFS(Node *root, int d)
{
if(root==NULL)
return 0;
dfs[len]=root->value;
depth[len]=d;
pos[root->value-1]=len; //
len++;
//DFS(root->left, ++d);
DFS(root->left, d+1);
// left backtrack
if(root->left!=NULL)
{
dfs[len]=root->value;
depth[len]=d;
len++;
}
//DFS(root->right, ++d);
DFS(root->right, d+1);
// right backtrack
if(root->right!=NULL)
{
dfs[len]=root->value;
depth[len]=d;
len++;
}
return 0;
}
//
int RMQ()
{
int i, j;
int lenJ=(int)sqrt(len);
for(i=0;ivalue-1], j=pos[b->value-1];
int temp;
if(i>j)
{
temp=j;
i=j;
j=temp;
}
int k=(int)sqrt(j-i+1);
// pay attention to the relation
int minpos=(depth[M[i][k]]value, b->value, nodeval);
return 0;
}
int main()
{
Node *n3 = new Node(3, NULL, NULL);
Node *n5 = new Node(5, NULL, NULL);
Node *n6 = new Node(6, NULL, NULL);
Node *n8 = new Node(8, NULL, NULL);
Node *n4 = new Node(4, n5, n6);
Node *n7 = new Node(7, NULL, n8);
Node *n2 = new Node(2, n3, n4);
Node *n1 = new Node(1, n2, n7);
DFS(n1, 0); // pass parameter depth
RMQ();
Query(n4, n8);
return 0;
}
构造的二叉树为:
// POJ 1330
// Use Tarjan to solve LCA problem
#include
#include
#define MAX 100
using namespace std;
int parent[MAX];
int rank[MAX];
int ancestor[MAX];
int indegree[MAX];
int visit[MAX]; // record
vector Tree[MAX], Ques[MAX];
// 加入初始化为多次运行清理环境
int Init(int n)
{
int i;
for(i=1;i<=n;i++)
{
parent[i]=0;
rank[i]=0;
ancestor[i]=0;
indegree[i]=0;
visit[i]=0;
Tree[i].clear();
Ques[i].clear();
}
return 0;
}
int MakeSet(int i)
{
parent[i]=i;
rank[i]=1;
//ancestor[i]=i;
return 0;
}
int Find(int i)
{
if(parent[i]==i)
return i;
else
parent[i]=Find(parent[i]);
return parent[i];
}
int Union(int x, int y)
{
// 寻找集合的根
int a=Find(x);
int b=Find(y);
if(a==b)
return 0;
else if(rank[a]>rank[b])
{
parent[b]=a;
rank[a]+=rank[b];
}
else
{
parent[a]=b;
rank[b]+=rank[a];
}
/*
else if(a!=b)
{
parent[b]=a;
rank[a]+=1;
}
*/
return 0;
}
int LCA(int u)
{
MakeSet(u);
ancestor[u]=u;
int size=Tree[u].size(); //
for(int i=0;i>cnt;
while(cnt--)
{
int s, t, i;
cin>>n;
//for(int i=0;i>s>>t;
Tree[s].push_back(t);
indegree[t]++;
}
cin>>s>>t;
Ques[s].push_back(t);
Ques[t].push_back(s); // 离线算法,两次询问
//for(int i=0;i
程序的运行效果如下:
参考资料:《RMQ和LCA讲稿》及网络上的各种资料文章