很好的参考资料:http://taop.marchtea.com/04.04.html 下面的配图和部分文字转载于此文章
离线算法就是指统一输入后再统一输出,而不是边输入边实时输出。Tarjan算法的复杂度为O(N+Q),Q为询问的次数.
由于是离线算法,所以要保存输入的信息,次序问题。
若两个结点u、v分别分布于某节点t 的左右子树,那么此节点 t即为u和v的最近公共祖先。更进一步,考虑到一个节点自己就是LCA的情况,得知:
•若某结点t 是两结点u、v的祖先之一,且这两结点并不分布于该结点t 的一棵子树中,而是分别在结点t 的左子树、右子树中,那么该结点t 即为两结点u、v的最近公共祖先。
这个定理就是Tarjan算法的基础。
引用 此文 中的一个例子。(蓝色字体是我加的说明)
STEP 1:从根结点1开始,开始访问结点1、2、3(祖先相同的节点在同一个集合)
STEP 2:2的左子树结点3访问完毕 (当左子树访问完毕后,这里是坐孩子,要将其祖先节点设为父节点,即ancestor[3]=2, 3,2成为一个集合)
STEP 3:开始访问2的右子树中的结点4、5、6
STEP 4:4的左子树中的结点5访问完毕
STEP 5:开始访问4的右子树的结点6
STEP 6:结点4的左、右子树均访问完毕,故4、5、6中任意两个结点的LCA均为4(当一个顶点的子树全部访问以后并返回该顶点时,才可以查询与该顶点有关的最近公共祖先信息,比如查询 4,5的公共祖先,这里用到了并查集,以4为根节点时,一开始father[4]=4( 一开始定义为-1也可以,只要有个唯一标识就行),4,5先合并,father[4]=5, 后来4,6合并,father[ find(4)] =6 ,即father[5]=6 ,当4,5的最近公共祖先即为 ancestor[ find (5)] =ancestor[6]= 4。)
STEP 7:2的左子树、右子树均访问完毕,故2、3、4、5、6任意两个结点的LCA均为2 (2,3,4,5,6在同一个集合中)
如上所述:进行到此step7,当访问完结点2的左子树(3),和右子树(4、5、6)后,结点2、3、4、5、6这5个结点中,任意两个结点的最近公共祖先均为2。
STEP 8:1的左子树访问完毕,开始访问1的右子树
STEP 9:开始访问1的右子树中的结点7、8
STEP 10
STEP 11
STEP 12:1的右子树中的结点7、8访问完毕
当进行到此step12,访问完1的左子树(2、3、4、5、6),和右子树(7、8)后,结点2、3、4、5、6、7、8这7个结点中任意两个结点的最近公共祖先均为1。
STEP 13:1的左子树、右子树均访问完毕(最后所有的节点都在同一个集合)
通过上述例子,我们能看到,使用此Tarjan算法能解决咱们的LCA问题。
过程总结:
上面的流程很清楚,当一个顶点的子树全部访问完并返回该顶点后,才能对涉及到该顶点的查询进行查询,这时候该顶点和子树有着共同的最近祖先,就是该顶点,也许有疑问,该顶点下面的顶点的最近公共祖先不一定是该顶点,比如上图2顶点下面的5,6,它们的最近公共祖先是4。这就是递归的奇妙之处了,它是从上到下,再从下向上返回,不断向上更新,集合也在不断的合并,最初解决的问题就是4,5,6这样的有叶子节点的子树,当返回4时,看看输入中和4相关的查询有没有,如果查询4,6那肯定是4,这时候还没有涉及到顶点2的相关查询,因为递归还没有返回到2,所以每返回到一个节点(节点就是顶点),就看看输入中有没有关于该节点相关的查询。当根节点的左子树全部访问后,左子树的所有节点和根节点的祖先变为了根节点,那么左子树的所有节点和根节点与右子树的任意节点的最近的公共祖先都为根节点。仔细品味一下,这个算法真的很奇妙。
实现方法:
这样的题目为每个顶点都建立邻接表,即保存与该顶点通过一条边直接相连的所有顶点,图是双向的,加边的时候要加正反两条边。
上面是根据给定的图为每个顶点建立邻接表,还要根据输入为每个顶点建立邻接表,还要记录该查询是第几次查询, 3,5 和 5,3的查询结果是一样的。
并查集使用的很巧妙,需要路径压缩,集合根据搜索返回从小不断扩大,包含在里面所有顶点的祖先也在实时更新,最后所有的顶点的祖先为根。
下面是bin神的模板:
POJ 1330
先输入t为测试数据组数,然后输入一个n,表示有n个顶点,接下来n-1行,表明有n-1条边,每行包括两个顶点u ,v ,接下来一行是需要查询的两个顶点u,v,即求u,v的最近公共祖先。本题本组测试数据只涉及到了一条查询。
maxn顶点最多个数,maxq最大查询条数.ans[i]保存第i次输入的查询的结果,i从0开始.
#include <iostream> #include <stdio.h> #include <algorithm> #include <string.h> using namespace std; const int maxn=10010;//顶点数 const int maxq=100;//最多查询次数,根据题目而定,本题中其实每组数据只有一个查询. //并查集 int f[maxn];//根节点 int find(int x) { if(f[x]==-1) return x; return f[x]=find(f[x]); } void unite(int u,int v) { int x=find(u); int y=find(v); if(x!=y) f[x]=y; } //并查集结束 bool vis[maxn];//节点是否访问 int ancestor[maxn];//节点i的祖先 struct Edge { int to,next; }edge[maxn*2]; int head[maxn],tot; void addedge(int u,int v)//邻接表头插法加边 { edge[tot].to=v; edge[tot].next=head[u]; head[u]=tot++; } struct Query { int q,next; int index;//查询编号,也就是输入的顺序 }query[maxq*2]; int ans[maxn*2];//存储每次查询的结果,下表0~Q-1,其实应该开maxq大小的。 int h[maxn],tt; int Q;//题目中需要查询的次数 void addquery(int u,int v,int index)//邻接表头插法加询问 { query[tt].q=v; query[tt].next=h[u]; query[tt].index=index; h[u]=tt++; query[tt].q=u;//相当于两次查询,比如查询 3,5 和5,3结果是一样的,以3为头节点的邻接表中有5,以5为头节点的邻接表中有3 query[tt].next=h[v]; query[tt].index=index; h[v]=tt++; } void init() { tot=0; memset(head,-1,sizeof(head)); tt=0; memset(h,-1,sizeof(h)); memset(vis,0,sizeof(vis)); memset(f,-1,sizeof(f)); memset(ancestor,0,sizeof(ancestor)); } void LCA(int u) { ancestor[u]=u; vis[u]=true; for(int i=head[u];i!=-1;i=edge[i].next)//和顶点u相关的顶点 { int v=edge[i].to; if(vis[v]) continue; LCA(v); unite(u,v); ancestor[find(u)]=u;//将u的左右孩子的祖先设为u } for(int i=h[u];i!=-1;i=query[i].next)//看输入的查询里面有没有和u节点相关的 { int v=query[i].q; if(vis[v]) ans[query[i].index]=ancestor[find(v)]; } } bool flag[maxn];//用来确定根节点的 int t; int n,u,v; int main() { int a,b,c; scanf("%d(%d):%d",&a,&b,&c); cout<<a<<b<<c; scanf("%d",&t); while(t--) { scanf("%d",&n); init(); memset(flag,0,sizeof(flag)); for(int i=1;i<n;i++) { scanf("%d%d",&u,&v); flag[v]=true;//有入度 addedge(u,v); addedge(v,u); } Q=1;//题目中只有一组查询 for(int i=0;i<Q;i++) { scanf("%d%d",&u,&v); addquery(u,v,i); } int root; for(int i=1;i<=n;i++) { if(!flag[i]) { root=i; break; } } LCA(root); for(int i=0;i<Q;i++) printf("%d\n",ans[i]); } return 0; }