转自:http://write.blog.csdn.net/postedit http://blog.csdn.net/smallacmer/article/details/7432625
离线算法(Tarjan算法)描述:
所谓离线算法,是指首先读入所有的询问(求一次LCA叫做一次询问),然后重新组织查询处理顺序以便得到更高效的处理方法。Tarjan算法是一个常见的用于解决LCA问题的离线算法,它结合了深度优先遍历和并查集,整个算法为线性处理时间。
Tarjan算法是基于并查集的,利用并查集优越的时空复杂度,可以实现LCA问题的O(n+Q)算法,这里Q表示询问 的次数。更多关于并查集的资料,可阅读这篇文章:《数据结构之并查集》。
同上一个算法一样,Tarjan算法也要用到深度优先搜索,算法大体流程如下:对于新搜索到的一个结点,首先创建由这个结点构成的集合,再对当前结点的每一个子树进行搜索,每搜索完一棵子树,则可确定子树内的LCA询问都已解决。其他的LCA询问的结果必然在这个子树之外,这时把子树所形成的集合与当前结点的集合合并,并将当前结点设为这个集合的祖先。之后继续搜索下一棵子树,直到当前结点的所有子树搜索完。这时把当前结点也设为已被检查过的,同时可以处理有关当前结点的LCA询问,如果有一个从当前结点到结点v的询问,且v已被检查过,则由于进行的是深度优先搜索,当前结点与v的最近公共祖先一定还没有被检查,而这个最近公共祖先的包涵v的子树一定已经搜索过了,那么这个最近公共祖先一定是v所在集合的祖先。
算法伪代码:LCA(u)
{
Make-Set(u)
ancestor[Find-Set(u)]=u
对于u的每一个孩子v
{
LCA(v)
Union(u,v)
ancestor[Find-Set(v)]=u
}
checked[u]=
true
对于每个(u,v)属于P
// (u,v)是被询问的点对
{
if
checked[v]=
true
then {
回答u和v的最近公共祖先为ancestor[Find-Set(v)]
}
}
}
以上转自:http://write.blog.csdn.net/postedit【举例说明】 根据实现算法可以看出,只有当某一棵子树全部遍历处理完成后, 才将该子树的根节点标记为黑色(初始化是白色),假设程序按上面的树形结构进行遍历,首先从节点1开始 ,然后递归处理根为2的子树,当子树2处理完毕后,节点2, 5, 6均为黑色;接着要回溯处理3子树,首先被染黑的是节点7 (因为节点7作为叶子不用深搜,直接处理),接着节点7就会查看所有询问(7, x)的节点对,假如存在(7, 5),因为节点5已经被染黑, 所以就可以断定(7, 5)的最近公共祖先就是find(5).ancestor,即节点1(因为2子树处理完毕后,子树2和节点1进行了union,find(5)返回了合并后的树的根1, 此时树根的ancestor的值就是1)。有人会问如果没有(7, 5), 而是有(5, 7)询问对怎么处理呢? 我们可以在程序初始化的时候做个技巧,将询问对(a, b)和(b, a)全部存储,这样就能保证完整性。
下面的文章转自:
http://blog.csdn.net/smallacmer/article/details/7432625LCA算法:
LCA(Least Common Ancestor),顾名思义,是指在一棵树中,距离两个点最近的两者的公共节点。也就是说,在两个点通往根的道路上,肯定会有公共的节点,我们就是要求找到公共的节点中,深度尽量深的点。还可以表示成另一种说法,就是如果把树看成是一个图,这找到这两个点中的最短距离。
LCA算法有在线算法也有离线算法,所谓的在线算法就是实时性的,比方说,给你一个输入,算法就给出一个输出,就像是http请求,请求网页一样。给一个实时的请求,就返回给你一个请求的网页。而离线算法则是要求一次性读入所有的请求,然后在统一得处理。而在处理的过程中不一定是按照请求的输入顺序来处理的。说不定后输入的请求在算法的执行过程中是被先处理的。
本文先介绍一个离线的算法,就做tarjan算法。这个算法是基于并查集和DFS的。Dfs的作用呢,就是递归,一次对树中的每一个节点进行处理。而并查集的作用就是当dfs每访问完(注意,这里是访问完)到一个点的时候,就通过并查集将这个点,和它的子节点链接在一起构成一个集合,也就是将并查集中的pnt值都指向当前节点。这样就把树中的节点分成了若干个的集合,然后就是根据这些集合的情况来对输入数据来进行处理。
比方说当前访问到的节点是u,等u处理完之后呢,ancestor[u]就构成了u的集合中的点与u点的LCA,而ancestor[fa[u]]就构成了,u的兄弟节点及其兄弟子树的集合中点与u的LCA,而ancestor[fa[fa[u]]]就构成了u的父亲节点的兄弟节点及其兄弟子树的集合中的点与u的LCA。然后依次类推,这样就构成了这个LCA的离线算法。
下面来分析一下代码:
int findp(int x)
{
if(pnt[x]!=x) pnt[x] = findp(pnt[x]);
return pnt[x];
}
int unionset(int x,int y)
{
int x = findp(x);
int y = findp(y);
pnt[y] = x;
} //以上两步是并查集的操作,没啥过多解释。
void Lcancestor(int parent)
{
pnt[parent] = parent; //当访问到一个点的时候,先将其自己形成一个集合
ancestor[findp(parent)] = parent;
for(int i=0;i<=child[parent].size();i++)
{ //接着一次访问节点的子节点,
Lcancestor(child[parent][i]); //依次对子节点进行访问。
unionset(parent,child[parent][i]); //在处理完后,将子节点的集合链接到父节点
ancestor[findp(child[parent][i])] = parent;
} //实际上这一步起到了并查集的压缩节点的作用。这样可以将查询降低到O(1)
color[parent] = true;
if( parent = first && color[second] ) //这里的first和second主要针对的是查询的每次操作时输入的两个数。
{
ans = ancestor[findp(second)] ;
}
if( parent = second && color[first] )
{
ans = ancestor[findp(first)];
}
}
LCA还有其他的算法,例如,将每个点到根节点的路径构成一个链表,那么LCA就是求两个链表的公共节点中位置最靠后的一个点。还有的LCA可以与RMQ问题结合起来,至于什么事RMQ问题,将会在下一篇博文中给出解释。
以下是针对上述两篇文章给的源代码:#define N 10 int pnt[N]; int anc[N]; bool checked[N]; int edge[N][N]; int findp(int x) { if(pnt[x]!=x) pnt[x]=findp(pnt[x]); return pnt[x]; } //x为y的父亲节点 void unionset(int x,int y) { x=findp(x); y=findp(y); pnt[y]=x; } int first; int second; int ans; void dfs(int parent) { pnt[parent]=parent; anc[findp(parent)]=parent; for(int i=0;i<N;i++) { if(parent!=i&&edge[parent][i]) { dfs(i); unionset(parent,findp(i)); anc[findp(i)]=parent; } } checked[parent]=true; if(parent==first&&checked[second]) ans=anc[findp(second)]; if(parent==second&&checked[first]) ans=anc[findp(first)]; } int main() { int x,y; for(int i=0;i<N-1;i++){ cin>>x>>y; edge[x][y]=1; } first=8; second=6; dfs(0); cout<<"祖先为:"<<ans<<endl; return 0; }