给出一棵有根数T,对于任意两个节点u,v,求出LCA(T,u,v),即距离树根最远的节点x(距离u和v的最近公共祖先节点),是的x同时是u和v的祖先
如上图所示,假设遍历完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
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有关的询问。
需要根据查询操作是否被处理过来决定,例如询问:1 2
像这种询问在处理1的时候2还没有被处理,所以的询问结果,所以应该添加双份询问(即再添加一条2 1,使得在处理2的时候可以处理1 2这条询问)
如果所有的询问经过处理,如:A B,在处理A是B都已经被处理过则无需添加双份询问
并查集的父节点和Tarjan算法需要使用的LCA的父节点集合(对查询的询问结果)分开存放比较好处理
利用深度优先遍历当前根节点的左右子节点,每处理完一棵字数就把它并到父亲所在的集合中,在任何时候一个集合里面的元素都形成了一棵树。因此对于任何处理过(即黑色)的节点v,(在处理u v询问的时候)v当前所在的集合代表元(集合根节点)就是v和当前处理节点u的LCA。
第一种先处理根节点所有子节点的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;
算法的时间复杂度取决于MAKE-SET,UNION和FIND-SET的实现细节(即并查集操作的时间复杂度)。
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/ (这里的动画演示可以更好地理解这一算法)