1.这个算法基于并查集和深度优先搜索。算法从根开始,对每一棵子树进行深度优先搜索,访问根时,将创建由根结点构建的集合,然后对以他的孩子结点为根的子树进行搜索,使对于 u, v 属于其某一棵子树的 LCA 询问完成。这时将其所有子树结点与根结点合并为一个集合。 对于属于这个集合的结点 u, v 其 LCA 必定是根结点。
2对于最近公共祖先问题,我们先来看这样一个性质,当两个节点(u,v)的最近公共祖先是x时,那么我们可以确定的说,当进行后序遍历的时候,必然先访问完x的所有子树,然后才会返回到x所在的节点。这个性质就是我们使用Tarjan算法解决最近公共祖先问题的核心思想。
同时我们会想这个怎么能够保证是最近的公共祖先呢?我们这样看,因为我们是逐渐向上回溯的,所以我们每次访问完某个节点x的一棵子树,我们就将该子树所有节点放进该节点x所在的集合,并且我们设置这个集合所有元素的祖先是该节点x。那么到我们完成对一个节点的所有子树的访问时,我们将这个节点标记为已经找到了祖先的点。
这个时候就体现了Tarjan采用离线的方式解决最近公共祖先的问题特点所在了,所以这个时候就体现了这一点。假设我们刚刚已经完成访问的节点是a,那么我们看与其一同被询问的另外一个点b是否已经被访问过了,若已经被访问过了,那么这个时候最近公共祖先必然是b所在集合对应的祖先c,因为我们对a的访问就是从最近公共祖先c转过来的,并且在从c的子树b转向a的时候,我们已经将b的祖先置为了c,同时这个c也是a的祖先,那么c必然是a、b的最近公共祖先。
对于一棵子树所有节点,祖先都是该子树的根节点,所以我们在回溯的时候,时常要更新整个子树的祖先,为了方便处理,我们使用并查集维护一个集合的祖先。总的时间复杂度是O(n+q)的,因为dfs是O(n)的,然后对于询问的处理大概就是O(q)的。
http://poetrinity.diandian.com/post/2012-02-04/19684915
LCA(最近公共祖先)问题
方法一:Tarjan离线算法
在学习离线算法的时候先需要先巩固一下深度搜索,并查集
Tarjan离线算法是基于深度优先搜索的,我们从根开始向下搜索,搜到一个节点的时候首先判断该节点所有子节点是否访问过,如果都已经访问过,则判断该节点是否询问点里面的其中一个,如果是,则判断跟它相对应的那个点是否已经访问过,如果访问过,则他们的最近公共祖先便是已经访问过的那个节点的当前节点,如果另外一个节点没有访问,则继续进行深度搜索。
例题:POJ 1330 Nearest Common Ancestors
这道题目是让求解最近公共祖先的问题,不过这道题目让我们求解的只有一组,对于离线算法而言,可以一下子求解多组,只要我们把需要求解的所有组建立一张表,当搜索到这个节点的时候去寻找跟这个节点相连的节点有没有被访问过的,如果访问过,则将他们的公共祖先先记录下来,这样到最后搜索完所有的点后就可以在O(1)的时间内输出所有需要求解最近公共祖先的解了。
#include #include using namespace std; const int size=10005; int T,N; vectorvec[size]; int father[size]; bool root[size]; bool visited[size]; int first,second; void In_data() { scanf("%d",&N); for(int i=1;i<=N;++i) { vec[i].clear(); father[i]=i; root[i]=true; visited[i]=false; } for(int i=1;i { int beg,end; scanf("%d%d",&beg,&end); vec[beg].push_back(end); root[end]=false; } scanf("%d%d",&first,&second); //记录两个节点 } int Find(int x) { if(father[x]==x) return x; else return father[x]=Find(father[x]); } void Union(int a,int b) { int A=Find(a); int B=Find(b); if(A!=B) father[B]=A; } void LCA(int parent) { for(int i=0;i { LCA(vec[parent][i]); Union(parent,vec[parent][i]); } visited[parent]=true; //确认该点已经搜索过 //如果目前的访问的节点就是其中一个节点,查看两外一个是否访问过 if(parent==first&&visited[second]==true) { printf("%d\n",Find(second)); return ; } else if(parent==second&&visited[first]==true) { printf("%d\n",Find(first)); return ; } } int main() { //freopen("1.txt","r",stdin); scanf("%d",&T); while(T--) { In_data(); //以根节点为入口 for(int i=1;i<=N;++i) { if(root[i]==true) { LCA(i); break; } } } }
方法二:在线算法
第二种求解最近公共祖先就是采用在线算法,这种算法可以融入ST算法来加快速度,在这儿先以一种简单的方法来对在线算法有个初步的认识。
在线算法首先也是给给出的边来建立一个图,找出根节点,然后深度搜索每一个节点,求出每一个节点的深度(根节点深度可以赋值为0),然后我们找到要求的那对点的深度,如果两者深度不一样,先让深度大的网上移动,直到两者深度一样,此时判断两者是否为同一个节点,如果不是同一个节点,就都将他们改变为他们的父节点,这样一步一步往上移动,最后当遇到两者一样的时候,这个点就是它们的根节点了。
同样是上面一道例题,在线算法的代码如下
#include #include using namespace std; const int size=10005; int T,N,first,second; vectorvec[size]; //用来记录图的信息 bool root[size]; //用来查找根节点 int depth[size]; //用来记录该节点的深度 int father[size]; //用来记录该节点的父节点 //录入数据 void In_data() { scanf("%d",&N); for(int i=1;i<=N;++i) { vec[i].clear(); depth[i]=0; root[i]=true; father[i]=i; } for(int i=1;i { int beg,end; scanf("%d%d",&beg,&end); vec[beg].push_back(end); father[end]=beg; root[end]=false; } scanf("%d%d",&first,&second); } //parent表示根节点,dep表示当前节点的深度 void depth_plus(int parent,int dep) { for(int i=0;i { depth_plus(vec[parent][i],dep+1); } depth[parent]=dep; } //在线算法查找最近公共祖先 int find_ancestor() { if(depth[first]>depth[second]) { while(depth[first]!=depth[second]) { first=father[first]; } } else if(depth[second]>depth[first]) { while(depth[first]!=depth[second]) { second=father[second]; } } while(first!=second) { first=father[first]; second=father[second]; } return first; } int main() { //freopen("1.txt","r",stdin); scanf("%d",&T); while(T--) { In_data(); //以根节点为入口,给每个点赋予深度 for(int i=1;i<=N;++i) { if(root[i]==true) { depth_plus(i,0); break; } } printf("%d\n",find_ancestor()); } }
采取上面一步一步往上推的在线算法是比较山寨的,时间可能比较慢点儿,所以我们就可以采用ST算法来对上面的方法进行一些预处理。当然有不好的地方必然也有点儿小优点。问题:现在给一个树添加一条边,你能求出这个树里面所新形成的环吗?输出这个环的各条边。对于这个问题我们就可以采用上面一步一步的向上搜索的方法,这样最后就能输出这个环了。当然要按问题讨论的,我们还是应该学一下ST算法来预处理下加速的。