LCA的Tarjan算法

1.背景简介

给出一棵有根数T,对于任意两个节点u,v,求出LCA(T,u,v),即距离树根最远的节点x(距离u和v的最近公共祖先节点),是的x同时是u和v的祖先

2.算法详解

LCA的Tarjan算法_第1张图片

如上图所示,假设遍历完10的孩子,要处理关于10的请求了,此时并查集所用的集合中共有四个集合:
    1,2,5,6为一个集合,祖先为1,集合中点和10的LCA为1
    3,7为一个集合,祖先为3,集合中点和10的LCA为3
    8,9,11为一个集合,祖先为8,集合中点和10的LCA为8
    10,12为一个集合,祖先为10,集合中点和10的LCA为10

2.1为什么此时会有四个集合,而不是以根节点10为首的集合和其它已经遍历过的点组成的两个集合呢?

原因:根据Tarjan算法利用深度优先遍历的原理,只有当当前根节点的所有子节点都遍历完之后才会形成一个以当前根节点为首的集合。
1)在遍历到节点10的时候,此时并没有回溯到10的根节点8,所以并没有完成节点10和节点8的合并,从而分成两个集合。
2)而在节点8因为子节点10没有遍历结束,所以节在点3看来他的子节点8也没有遍历结束,因此也没有完成节点3和节点8的合并,从而又分出一个集合。
3)而节点1因为子节点3没有遍历结束,所以在节点1看来他的子节点3也没有遍历结束,因此也没有完成节点1和节点3的合并,从而又分出一个集合。
此时,并查集所使用的集合中就包含了四个集合,从上到下一次标定为A(根为1),B(根为3),C(根为8),D(根为10)。
    当在处理询问(A,B)的时候,如果B没有遍历过则可以发现B不属于A,B,C,D任何一个集合,不能回答询问,此时只能等到处理B的时候才可以回答。如果B已经被处理过,则B肯定属于A,B,C,D其中的某一个集合,则B所在集合的根节点就是A和B的最近公共祖先节点。

2.2为什么要用祖先而且每次合并集合后都要确保集合的祖先正确呢?

因为集合是用并查集实现的,为了提高速度,当然要平衡加路径压缩了,所以合并后谁是根就不确定了,所以要始终保持集合的根的祖先是正确的。

2.3关于查询和遍历孩子的顺序

wikipedia上就是上文中的顺序,很多人的代码也是这个顺序
但是网上的很多讲解却是查询在前,遍历孩子在后,对比上文,会不会漏掉u和u的子孙之间的查询呢?
答案是:不会漏掉任何一个查询,因为我们在处理的是关于节点u的查询,和节点u的子节点无关,当需要处理关于节点u的字节点的询问的时候,可以等到遍历到子节点的时候处理(即如果询问在前的话,需要添加双份询问)。而且在处理(u,v)询问的时候,主要考察的是节点v是否被处理过(即节点v是否已经在集合中)来计算(u,v)的最近公共祖先的。

在采用第二种模板的时候,处理一下询问:
3 2:此时2已经处理完,并且已经和根节点1合并,所以LCA(3,2)=ans[find(2)]=1;
9 7:由于此时3的子节点尚未处理完,尚未完成9和根节点3的合并,但是此时7已经和3合并完成,所以LCA(9,7)=ans[find(7)]=3;
精髓就在于,Tarjan算法是当本跟根节点的所有子节点全部处理完之后才完成根节点和子节点的合并,从而保证,所有询问的答案都在从根节点1到当前节点的路径上。
而且利用第二种模板中先设置vit[u]=true,从而保证在询问的时候能处理和u有关的询问。

2.4是否需要添加双份的询问操作呢?

需要根据查询操作是否被处理过来决定,例如询问:1 2
像这种询问在处理1的时候2还没有被处理,所以的询问结果,所以应该添加双份询问(即再添加一条2 1,使得在处理2的时候可以处理1 2这条询问)
如果所有的询问经过处理,如:A B,在处理A是B都已经被处理过则无需添加双份询问

2.5算法中并查集所用的父节点集合和Tarjan算法中使用的询问结果使用的父节点集合是一样的吗?

并查集的父节点和Tarjan算法需要使用的LCA的父节点集合(对查询的询问结果)分开存放比较好处理

3.算法思想

利用深度优先遍历当前根节点的左右子节点,每处理完一棵字数就把它并到父亲所在的集合中,在任何时候一个集合里面的元素都形成了一棵树。因此对于任何处理过(即黑色)的节点v,(在处理u v询问的时候)v当前所在的集合代表元(集合根节点)就是v和当前处理节点u的LCA。

4.程序模板

 第一种先处理根节点所有子节点的LCA询问,然后再处理关于根节点的询问(推荐用法)

int f[maxn],fs[maxn];//并查集父节点 父节点个数 
bool vit[maxn]; 
int anc[maxn];//祖先 
vector<int> son[maxn];//保存树 
vector<int> qes[maxn];//保存查询 
typedef vector<int>::iterator IT; 
  
int Find(int x) 
{ 
    if(f[x]==x) return x; 
    else return f[x]=Find(f[x]); 
} 
void Union(int x,int y) 
{ 
    x=Find(x);y=Find(y); 
    if(x==y) return; 
    if(fs[x]<=fs[y]) f[x]=y,fs[y]+=fs[x]; 
    else f[y]=x,fs[x]+=fs[y]; 
} 
  
void lca(int u) 
{ 
    //vit[u]=true;
    anc[u]=u; 
    for(IT v=son[u].begin();v!=son[u].end();++v) 
    { 
        lca(*v); 
        Union(u,*v); 
        anc[Find(u)]=u; 
    } 
    vit[u]=true;//这句话也可以放到上面,因为上面的代码中根本没有用到vit[u],所以不影响程序结果,这样就和下面的模板方法比较类似了
    for(IT v=qes[u].begin();v!=qes[u].end();++v) 
    { 
        if(vit[*v]) 
            printf("LCA(%d,%d):%d\n",u,*v,anc[Find(*v)]); 
    } 
}

第二种先处理关于根节点的询问,然后再处理根节点所有子节点的LCA询问

//parent为并查集,FIND为并查集的查找操作
//QUERY为询问结点对集合
//TREE为基图有根树
 Tarjan(u)
      visit[u] = true
      for each (u, v) in QUERY
          if visit[v]
              ans(u, v) = FIND(v)
      anc[Find(u)]=u;
      for each (u, v) in TREE
              if !visit[v]
              Tarjan(v)
              unin(u,v);
              anc[Find(u)]=u;


5.复杂度

算法的时间复杂度取决于MAKE-SET,UNION和FIND-SET的实现细节(即并查集操作的时间复杂度)。

6.参考文献

http://scturtle.is-programmer.com/posts/30055.html (第一种模板写法)
http://blog.csdn.net/limchiang/article/details/8100531 第一种模板写法的验证程序
http://www.cnblogs.com/ylfdrib/archive/2010/11/03/1867901.html (第二种模板写法)
http://asongofcode.com/?p=18 (第二种模板写法的实际解决题目代码参考)
http://www.cweye.net/archives/32 (第二种模板写法的实际解决题目代码参考)
http://blog.csdn.net/dgq8211/article/details/7828478  第二种模板写法的验证程序
http://comzyh.tk/blog/archives/492/  (这里的动画演示可以更好地理解这一算法)

你可能感兴趣的:(算法,it,tarjan算法,算法框架)