首先请看定义:
一、最近公共祖先(Least Common 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问题的ST算法
const int MAXN=100000+1;
const int MAXF=17;
const int INF=0x7FFFFFFF;
//可?以?断?言?ceiil(log(MAXN,2))==MAXF
inline int max(int a,int b){return a>b?a:b;}
inline int min(int a,int b){return a<b?a:b;}
class{
int dp_max[MAXN][MAXF+1];//dp[i][j]表?示?从?a[i]起?连?续?^j次?方?个?数?的?最?大?值?
int dp_min[MAXN][MAXF+1];
public:
void init(int* a,int n){
for(int i=0;i<n;i++){
dp_max[i][0]=a[i];
dp_min[i][0]=a[i];
}
for(int f=1,s=1;s<n;s=(1<<f++)){
for(int i=0;i+s<n;i++){
dp_max[i][f]=max(dp_max[i][f-1],dp_max[i+s][f-1]);
dp_min[i][f]=min(dp_min[i][f-1],dp_min[i+s][f-1]);
}
}
}
int query_max(int l,int r){
if(l>r)return -INF;
int d=r-l+1;
int f;
for(f=0;(1<<f)<=d;f++);
f--;
return max(dp_max[l][f],dp_max[r-(1<<f)+1][f]);
}
int query_min(int l,int r){
if(l>r)return -INF;
int d=r-l+1;
int f;
for(f=0;(1<<f)<=d;f++);
f--;
return min(dp_min[l][f],dp_min[r-(1<<f)+1][f]);
}
}RMQ;
来看一下ST算法是怎么实现的(以最小值为例)最小值只需将min换成max即可:
首先是预处理,用一个DP解决。设a[i]是要求区间最值的数列,f[i,j]表示从第i个数起连续2^j个数中的最小值。例如数列3 2 4 5 6 8 1 2 9 7 ,f[1,0]表示第1个数起,长度为2^0=1的最小值,其实就是3这个数。f[1,2]=5,f[1,3]=8,f[2,0]=2,f[2,1]=4……从这里可以看出f[i,0]其实就等于a[i]。这样,Dp的状态、初值都已经有了,剩下的就是状态转移方程。我们把f[i,j]平均分成两段(因为f[i,j]一定是偶数个数字),从i到i+2^(j-1)-1为一段,i+2^(j-1)到i+2^j-1为一段(长度都为2^(j-1))。用上例说明,当i=1,j=3时就是3,2,4,5 和 6,8,1,2这两段。f[i,j]就是这两段的最小值中的最小值。于是我们得到了动规方程F[i,j]=min(F[i,j-1],F[i+2^(j-i),j-1]).接下来是得出最值,也许你想不到计算出f[i,j]有什么用处,一般毛想想计算max还是要O(logn),甚至O(n)。但有一个很好的办法,做到了O(1)。还是分开来。如在上例中我们要求区间[2,8]的最小值,就要把它分成[2,5]和[5,8]两个区间,因为这两个区间的最小值我们可以直接由f[2,2]和f[5,2]得到。扩展到一般情况,就是把区间[l,r]分成两个长度为2^n的区间(保证有f[i,j]对应)。直接给出表达式:
k:=trunc(l(r-l+1)/ln(2));ans:=min(F[l,k],F[r-2^k+1,k]);这样就计算了从i开始,长度为2^t次的区间和从r-2^i+1开始长度为2^t的区间的最小值(表达式比较烦琐,细节问题如加1减1需要仔细考虑
二. LCA问题的Tarjan离线算法
int tree[10001][100],in[10001],p[10001];
int cas,s,t;
int n,Q1,Q2;
bool v[10001];
void Make_Set(int t)
{
p[t]=t;
}
int Find_Set(int t)
{
if(t!=p[t])
{
p[t]=Find_Set(p[t]);
}
return p[t];
}
void Union(int u,int v)
{
p[v]=u;
}
int LCA(int u)
{
Make_Set(u);
int i;
for(i=1;i<=tree[u][0];i++)
{
LCA(tree[u][i]);
Union(u,tree[u][i]);
}
v[u]=1;
if(u==Q1&&v[Q2])
{
printf("%d\n",p[Find_Set(Q2)]);
}
else if(u==Q2&&v[Q1])
{
printf("%d\n",p[Find_Set(Q1)]);
}
return 0;
}
Tarjan算法基于深度优先搜索的框架,对于新搜索到的一个结点,首先创建由这个结点构成的集合,再对当前结点的每一个子树进行搜索,每搜索完一棵子树,则可确定子树内的LCA询问都已解决。其他的LCA询问的结果必然在这个子树之外,这时把子树所形成的集合与当前结点的集合合并,并将当前结点设为这个集合的祖先。之后继续搜索下一棵子树,直到当前结点的所有子树搜索完。这时把当前结点也设为已被检查过的,同时可以处理有关当前结点的LCA询问,如果有一个从当前结点到结点v的询问,且v已被检查过,则由于进行的是深度优先搜索,当前结点与v的最近公共祖先一定还没有被检查,而这个最近公共祖先的包涵v的子树一定已经搜索过了,那么这个最近公共祖先一定是v所在集合的祖先。
最后讲解他们的转换关系
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,1R[i]为:1,2,4,5,3
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问题的规模同次。
再举一个例子帮助理解:
(1)
/ \(2) (7)
/ \ \
(3) (4) (8)
/ \
(5) (6)
一个nlogn 预处理,O(1)查询的算法.
Step 1:
按先序遍历整棵树,记下两个信息:结点访问顺序和结点深度.
如上图:
结点访问顺序是: 1 2 3 2 4 5 4 6 4 2 1 7 8 7 1 //共2n-1个值
结点对应深度是: 0 1 2 1 2 3 2 3 2 1 0 1 2 1 0
Step 2:
如果查询结点3与结点6的公共祖先,则考虑在访问顺序中
3第一次出现,到6第一次出现的子序列: 3 2 4 5 4 6.
这显然是由结点3到结点6的一条路径.
在这条路径中,深度最小的就是最近公共祖先(LCA). 即
结点2是3和6的LCA.
Step 3:
于是问题转化为, 给定一个数组R,及两个数字i,j,如何找出
数组R中从i位置到j位置的最小值..
如上例,就是R[]={0,1,2,1,2,3,2,3,2,1,0,1,2,1,0}.
i=2;j=7;
接下来就是经典的RMQ问题.
总结:
RMQ是给定一列数,动态询问[i,j]区间内的最小(或最大值)。
LCA是给定一棵树,动态询问u和v的最近公共祖先。解决这两种问题都有个很重要的倍增思想(这个思想在后缀数组方面亦有所应用)。
关键需要记住的是
在LCA预处理的时候
p[i,j] 表示i的2^j 倍祖先
那么就有一个递推式子 p[i,j]=p[p[i,j-1],j-1]
RMQ和LCA可以相互转化。。 所以只要记住一种就行了。。
RMQ转LCA的时候是生成一棵类似于堆的递归树;LCA转RMQ的时候用到的是深度优先遍历。
主要掌握的不在于算法,而是在于倍增思想附三份源代码
//POJ_3264 RMQ ST算?法?
#include <iostream>
using namespace std;
const int MAXN=100000+1;
const int MAXF=17;
const int INF=0x7FFFFFFF;
//可?以?断?言?ceiil(log(MAXN,2))==MAXF
inline int max(int a,int b){return a>b?a:b;}
inline int min(int a,int b){return a<b?a:b;}
class{
int dp_max[MAXN][MAXF+1];//dp[i][j]表?示?从?a[i]起?连?续?^j次?方?个?数?的?最?大?值?
int dp_min[MAXN][MAXF+1];
public:
void init(int* a,int n){
for(int i=0;i<n;i++){
dp_max[i][0]=a[i];
dp_min[i][0]=a[i];
}
for(int f=1,s=1;s<n;s=(1<<f++)){
for(int i=0;i+s<n;i++){
dp_max[i][f]=max(dp_max[i][f-1],dp_max[i+s][f-1]);
dp_min[i][f]=min(dp_min[i][f-1],dp_min[i+s][f-1]);
}
}
}
int query_max(int l,int r){
if(l>r)return -INF;
int d=r-l+1;
int f;
for(f=0;(1<<f)<=d;f++);
f--;
return max(dp_max[l][f],dp_max[r-(1<<f)+1][f]);
}
int query_min(int l,int r){
if(l>r)return -INF;
int d=r-l+1;
int f;
for(f=0;(1<<f)<=d;f++);
f--;
return min(dp_min[l][f],dp_min[r-(1<<f)+1][f]);
}
}RMQ;
int main()
{
int n,q;
int i,j,k;
int a[50005];
freopen("input.txt", "r", stdin);
freopen("output.txt", "w", stdout);
while (scanf("%d%d", &n, &q) !=EOF)
{
for (i=0; i<n; i++)
{
scanf("%d", &a[i]);
}
RMQ.init(a,n);
for (i=0; i<q; i++)
{
scanf("%d%d", &j, &k);
int t = RMQ.query_max(j-1,k-1);
int s = RMQ.query_min(j-1,k-1);
printf("%d\n",t-s);
}
}
return 0;
}
//POJ_ 1330 lCA转?RMQ
#include <iostream>
#include <vector>
using namespace std;
#define MAXN 20005
//LCA
int parent[MAXN];
vector<int> son[MAXN];
vector<int> E,D;
int R[MAXN*2];
const int MAXF=17;
const int INF=0x7FFFFFFF;
//可?以?断?言?ceiil(log(MAXN,2))==MAXF
inline int min(int a,int b){return a<b?a:b;}
class{
int dp_min[MAXN][MAXF+1]; //dp[i][j]表?示?从?a[i]起?连?续?^j次?方?个?数?的?最?大?值?
public:
void init(int n){
for(int i=0;i<n;i++){
dp_min[i][0]=D[i];
}
for(int f=1,s=1;s<n;s=(1<<f++)){
for(int i=0;i+s<n;i++){
dp_min[i][f]=min(dp_min[i][f-1],dp_min[i+s][f-1]);
}
}
}
int query_min(int l,int r){
if(l>r)return -INF;
int d=r-l+1;
int f;
for(f=0;(1<<f)<=d;f++);
f--;
return min(dp_min[l][f],dp_min[r-(1<<f)+1][f]);
}
int query_min_index(int l, int r)
{
int v = query_min(l,r);
if(v == -INF)return -INF;
int i;
for (i=l; i<=r; i++)
{
if (D[i] == v) return i;
}
}
}RMQ;
void DFS(int root, int deep)
{
int i;
E.push_back(root);
R[root] = E.size()-1;
D.push_back(deep);
for (i=0; i<son[root].size(); i++)
{
DFS(son[root][i],deep+1);
E.push_back(root);
D.push_back(deep);
}
}
int LCA(int l, int r)
{
RMQ.init(D.size());
int x = R[l];
int y = R[r];
if (x>y)
{
int temp = x;
x = y;
y= temp;
}
return E[RMQ.query_min_index(x,y)];
}
int main()
{
freopen("input.txt", "r", stdin);
freopen("output.txt", "w", stdout);
int N;
int n;
int i,j,k;
int a,b;
scanf("%d",&N);
for (i=0; i<N; i++)
{
scanf("%d", &n);
for (j=0; j<=n; j++)
{
parent[j] = j;
son[j].clear();
}
E.clear();
D.clear();
for (j=0; j<n-1; j++)
{
scanf("%d%d", &a,&b);
a--;
b--;
parent[b] = a;
son[a].push_back(b);
}
int root = 0;
while (parent[root] != root) root = parent[root];
DFS(root,0);
scanf("%d%d", &a,&b);
a--;
b--;
printf("%d\n",LCA(a,b)+1);
}
return 0;
}
//POJ_1330 LCA问?题?的?Tarjan离?线?算?法?
#include<iostream>
using namespace std;
int tree[10001][100],in[10001],p[10001];
int cas,s,t;
int n,Q1,Q2;
bool v[10001];
void Make_Set(int t)
{
p[t]=t;
}
int Find_Set(int t)
{
if(t!=p[t])
{
p[t]=Find_Set(p[t]);
}
return p[t];
}
void Union(int u,int v)
{
p[v]=u;
}
int LCA(int u)
{
Make_Set(u);
int i;
for(i=1;i<=tree[u][0];i++)
{
LCA(tree[u][i]);
Union(u,tree[u][i]);
}
v[u]=1;
if(u==Q1&&v[Q2])
{
printf("%d\n",p[Find_Set(Q2)]);
}
else if(u==Q2&&v[Q1])
{
printf("%d\n",p[Find_Set(Q1)]);
}
return 0;
}
int main()
{
scanf("%d",&cas);
while(cas--)
{
int i;
memset(tree,0,sizeof(tree));
memset(in,0,sizeof(in));
memset(v,0,sizeof(v));
scanf("%d",&n);
for(i=1;i<n;i++)
{
scanf("%d%d",&s,&t);
tree[s][++tree[s][0]]=t;
in[t]++;
}
scanf("%d%d",&Q1,&Q2);
for(i=1;i<=n;i++)
{
if(in[i]==0)
break;
}
LCA(i);
}
system("pause");
return 0;
}