tarjan算法求最近公共祖先
tarjian算法
LCA: LCA(Least Common Ancestor),顾名思义,是指在一棵树中,距离两个点最近的两者的公共节点。也就是说,在两个点通往根的道路上,肯定会有公共的节点,我们就是要求找到公共的节点中,深度尽量深的点。还可以表示成另一种说法,就是如果把树看成是一个图,这找到这两个点中的最短距离。
LCA算法有在线算法也有离线算法,所谓的在线算法就是实时性的,而离线算法则是要求一次性读入所有的请求,然后在统一得处理。而在处理的过程中不一定是按照请求的输入顺序来处理的。说不定后输入的请求在算法的执行过程中是被先处理的。
tarjan算法。这个算法是基于并查集和DFS的。离线算法。
现在我们来观察正在处理与x结点关联的询问时并查集的情况。由于一个结点处理完毕后,它就被归到其父结点所在的集合,所以在已经处理过的结点中(包括 x本身),x结点本身构成了与x的LCA是x的集合,x结点的父结点及以x的所有已处理的兄弟结点为根的子树构成了与x的LCA是father[x]的集合,x结点的父结点的父结点及以x的父结点的所有已处理的兄弟结点为根的子树构成了与x的LCA是father[father[x]]的集合……(上面这几句话如果看着别扭,就分析一下句子成分,也可参照右面的图)假设有一个询问(x,y)(y是已处理的结点),在并查集中查到y所属集合的根是z,那么z 就是x和y的LCA,x到y的路径长度就是 lv[x]+lv[y]-lv[z]*2 。累加所有经过的路径长度就得到答案。 现在还有一个问题:上面提到的询问(x,y)中,y是已处理过的结点。那么,如果y尚未处理怎么办?其实很简单,只要在询问列表中加入两个询问(x, y)、(y,x),那么就可以保证这两个询问有且仅有一个被处理了(暂时无法处理的那个就pass掉)。而形如(x,x)的询问则根本不必存储。 如果在并查集的实现中使用路径压缩等优化措施,一次查询的复杂度将可以认为是常数级的,整个算法也就是线性的了。
Tarjan作为离线off-line算法,在程序开始前,需要将所有等待询问的节点对提前存储,然后程序从树根开始执行TarjanLCA()。假如有如下一棵多叉树
根据TarjanLCA的实现算法可以看出,只有当某一棵子树全部遍历处理完成后,才将该子树的根节点标记为黑色(初始化是白色),假设程序按上面的树形结构进行遍历,首先从节点1开始,然后递归处理根为2的子树,当子树2处理完毕后,节点2, 5, 6均为黑色;接着要回溯处理3子树,首先被染黑的是节点7(因为节点7作为叶子不用深搜,直接处理),接着节点7就会查看所有询问(7, x)的节点对,假如存在(7, 5),因为节点5已经被染黑,所以就可以断定(7, 5)的最近公共祖先就是find(5).ancestor,即节点1(因为2子树处理完毕后,子树2和节点1进行了union,find(5)返回了合并后的树的根1,此时树根的ancestor的值就是1)。 有人会问如果没有(7, 5),而是有(5, 7)询问对怎么处理呢?我们可以在程序初始化的时候做个技巧,将询问对(a, b)和(b, a)全部存储,这样就能保证完整性。
如果要按顺序输出,在输入询问时打一个标记,假设开一个数组 f[][] , f[x][] = "顺序",
que[x][i]=y 是询问x,y,与x有关的第 i 个数是y,f[x][i] = "顺序" ,与x有关的第 i 个询问是的几个顺序,所以就可以表示输出的顺序了。
模板
1 #include<iostream> 2 #include<cstdio> 3 #include<algorithm> 4 #include<vector> 5 #include<cstring> 6 using namespace std; 7 8 const int N = 10010; 9 10 vector<int>node[N]; //相连的边 11 vector<int>que[N]; //询问 12 int n,m,x,y; 13 int far[N],ance[N]; //祖先,集合 14 int rank[N],ru[N]; //树的秩,入度 15 bool vis[N]; //判断是否访问过 16 17 void init() 18 { 19 for(int i=0;i<=n;i++) 20 { 21 node[i].clear(); 22 que[i].clear(); 23 far[i] = i; 24 rank[i] = 1; 25 } 26 } 27 28 int find(int x) 29 { 30 return far[x]==x?x:far[x]=find(far[x]); 31 } 32 33 void unio(int a,int b) 34 { 35 a=find(a); 36 b=find(b); 37 if(a==b) return ; 38 if(rank[a]<=rank[b]) 39 { 40 far[a] = b; 41 rank[b] += rank[a]; 42 } 43 else 44 { 45 far[b] = a; 46 rank[a] += rank[b]; 47 } 48 } 49 50 void LCA(int root) 51 { 52 ance[root] = root; //首先自成一个集合 53 for(int i=0;i<node[root].size();i++) 54 { 55 LCA(node[root][i]); //递归子树 56 unio(root,node[root][i]); //将子树节点与根节点root的集合合并 57 ance[find(root)] = root; //合并后的集合的祖先为root 58 } 59 vis[root] = true; 60 for(int i=0;i<que[root].size();i++) 61 { 62 if(vis[que[root][i]]) //如果另一节点已经访问过 63 printf("%d和%d的最近公共祖先:%d\n", root,que[root][i],ance[find(que[root][i])]); 64 } 65 } 66 67 int main() 68 { 69 scanf("%d%d",&n,&m); 70 init(); //调试了好久,原来这里的初始化要放在输入n下面 71 for(int i=1;i<n;i++) 72 { 73 scanf("%d%d",&x,&y); //x->y有一条边 74 node[x].push_back(y); 75 //node[y].push_back(x); 76 ru[y]++; 77 } 78 for(int i=1;i<=m;++i) 79 { 80 scanf("%d%d",&x,&y); 81 que[x].push_back(y); 82 que[y].push_back(x); 83 } 84 for(int i=0;i<=n;++i) 85 if(ru[i]==0) LCA(i); //寻找根节点 86 return 0; 87 }