两个月前有一次一个电话面试问到了一个问题:“怎样求二叉树中距离两个叶子节点最近的祖先节点。”当时不会,后来在网上查了查发现是一个比较经典的题目,也有几种算法可以解决这个问题,我学习了一下,在这儿记下来。这个问题更宽泛的定义是:如何求树(不限于二叉树)中两个节点(不限于叶子节点)的最近公共祖先节点。这个问题被称为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<i,那么答案为RMQ(Ak+1...N, i, j);
不难发现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),则这个算法的时间复杂度标记为:<f(n), g(n)>。
当然,最简单的算法是O(n)的,但是对于查询次数很多m(假设有100万次),则这个算法的时间复杂度为O(mn),显然时间效率太低。可以用线段树将查询算法优化到O(logn),而线段树的预处理时间复杂度为O(n),线段树整体复杂度为<O(n), O(logn)>,这个还没有研究。不过,Sparse_Table算法(简称ST算法)才是最好的:它可以在O(nlogn)的预处理以后实现O(1)的查询效率,即整体时间复杂度为<O(nlogn),O(1)>。ST(Sparse Table)算法的基本思想是,预先计算从起点A[i]开始长度为2的j次方
(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 <stdio.h> #include <math.h> #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<len;i++) M[i][0]=i; for(j=1;j<=lenJ;j++) { for(i=0;i+(1<<j)-1<len;i++) M[i][j]=(a[M[i][j-1]]<a[M[i+(1<<(j-1))][j-1]])?(M[i][j-1]):(M[i+(1<<(j-1))][j-1]); } return 0; } int Query(int a[], int i, int j) { int k=sqrt(j-i+1); //int k=log(j-i+1)/log(2); int min=(a[M[i][k]]<a[M[j+1-(1<<k)][k]])?(a[M[i][k]]):(a[M[j+1-(1<<k)][k]]); printf("The Minimum number denoted by %d and %d in the array is %d\n",i, j, min); return 0; } int main() { int Array[]={2,4,3,1,6,7,8,9,1,7}; int length=sizeof(Array)/sizeof(int); RMQinit(Array, length); Query(Array, 3, 7); return 0; }
// 二叉树LCA在线算法, 转化为RMQ问题 // c/c++是大小写敏感的, NULL=0, null只是一个符号 #include <iostream> #include <cmath> #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;i<len;i++) M[i][0]=i; for(j=1;j<=lenJ;j++) { for(i=0;i+(1<<j)-1<len;i++) M[i][j]=(depth[M[i][j-1]]<depth[M[i+(1<<(j-1))][j-1]])?(M[i][j-1]):(M[i+(1<<(j-1))][j-1]); } return 0; } int Query(Node *a, Node *b) { int i=pos[a->value-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]]<depth[M[j+1-(1<<k)][k]])?(M[i][k]):(M[j+1-(1<<k)][k]); int nodeval=dfs[minpos]; printf("The LCA of node %d and node %d is node %d", a->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 <iostream> #include <vector> #define MAX 100 using namespace std; int parent[MAX]; int rank[MAX]; int ancestor[MAX]; int indegree[MAX]; int visit[MAX]; // record vector<int> 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<size;i++) { LCA(Tree[u][i]); //Union(u,i); Union(u, Tree[u][i]); ancestor[Find(u)]=u; } visit[u]=1; size=Ques[u].size(); // 统计与u相关的查询的个数 for(int i=0;i<size;i++) { if(visit[Ques[u][i]]==1) { //cout<<ancestor[find(u)]<<endl; cout<<ancestor[Find(Ques[u][i])]<<endl; return 0; } } return 0; } int main() { int cnt; // number of Test cases need to be sloved int n; // number of nodes in a tree cin>>cnt; while(cnt--) { int s, t, i; cin>>n; //for(int i=0;i<n;i++) Init(n); for(i=0;i<n-1;i++) // N nodes then N-1 edges 此处i只为计数没有实际意义 { cin>>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<n;i++) // nodes start at 1 此处i为节点 for(i=1;i<=n;i++) { if(indegree[i]==0) { LCA(i); break; } } } return 0; }程序的运行效果如下:
参考资料:《RMQ和LCA讲稿》及网络上的各种资料文章