给一棵树,问两个节点的最近公共祖先是什么。
首先要说明的是,tarjan算法是离线的,它一次性读入所有的询问,而且不一定按照读入顺序来处理,但这也是算法的精妙之处;这个算法基于dfs和并查集。
每当处理好一颗子树的时候,我们就把他们归并到一个集合里,显然如果询问的两个点是在一颗子树里,那么他们的LCA就是这个子树的根,如果不是的话,就可能是这颗子树的根的根(的根...的根)
存储询问也很特别,例如询问a,b,算法把a,b b,a都存下来,保证完整性
接下来我就具体的一棵树来讲解:
从1开始搜索,我们假设路径为1-2-5-9,接着先判断,在9这个子树(其实是一个叶子)有没有要问的,显然不会有,因为除了这里处理完了,其他地方都没处理,接着回溯上来以后,合并5,9,递归到10,假如这时候询问9,10 或者 10,9 那么答案就已经计算出来了,就是5,也就是(5,9,10)这颗子树的根,如果没有询问,那么以后涉及到这颗子树的某个点的话,它们的LCA必定不是5,一定是5的祖先(的祖先....的祖先),所以当从5回溯到2的时候,我们就要把5和2合并起来,然后把5的祖先修改成2,然后再去递归6,其他过程也就类似了。
附:POJ1330代码:点击打开链接
#include<stdio.h> #include<string.h> #include<iostream> #include<algorithm> #include<vector> using namespace std; const int maxn=10005; vector<int>tree[maxn],que[maxn];//树和询问 bool vis[maxn]; int father[maxn]; int ancestor[maxn]; int rank[maxn]; int in[maxn];//入度,找根 int n; void init() { for (int i=1;i<=n;i++) { rank[i]=1; father[i]=i; in[i]=0; vis[i]=0; ancestor[i]=0; que[i].clear(); tree[i].clear(); } } int find(int x) { if (x==father[x]) return x; father[x]=find(father[x]); return father[x]; } void Union(int x,int y) { int a=find(x); int b=find(y); if (a!=b) { if (rank[a]<=rank[b]) //合并的时候按深度浅的向深的合并 { father[a]=b; rank[b]+=rank[a]; } else { father[b]=a; rank[a]+=rank[b]; } } } void LCA(int root) { ancestor[root]=root; int size=tree[root].size(); for (int i=0;i<size;i++) { LCA(tree[root][i]); Union(root,tree[root][i]); //printf("%d %d %d %d\n",root,tree[root][i],find(root),ancestor[find(root)]); ancestor[find(root)]=root; } vis[root]=1; size=que[root].size(); for (int i=0;i<size;i++) { if (vis[que[root][i]]) { printf("%d\n",ancestor[find(que[root][i])]); } } } int main() { int t; scanf("%d",&t); while (t--) { int x,y; scanf("%d",&n); init(); for (int i=0;i<n-1;i++) { scanf("%d%d",&x,&y); tree[x].push_back(y); in[y]++; } scanf("%d%d",&x,&y); que[x].push_back(y); que[y].push_back(x); for (int i=1;i<=n;i++) { if (in[i]==0) { LCA(i); break; } } } return 0; }