LCA 最近公共祖先 tarjan离线

在网上找了一些对tarjan算法解释较好的文章 并加入了自己的理解

 

LCA(Least Common Ancestor),顾名思义,是指在一棵树中,距离两个点最近的两者的公共节点。也就是说,在两个点通往根的道路上,肯定会有公共的节点,我们就是要求找到公共的节点中,深度尽量深的点。还可以表示成另一种说法,就是如果把树看成是一个图,这找到这两个点中的最短距离。

      LCA算法有在线算法也有离线算法,所谓的在线算法就是实时性的,比方说,给你一个输入,算法就给出一个输出,就像是http请求,请求网页一样。给一个实时的请求,就返回给你一个请求的网页。而离线算法则是要求一次性读入所有的请求,然后在统一得处理。而在处理的过程中不一定是按照请求的输入顺序来处理的。说不定后输入的请求在算法的执行过程中是被先处理的。

      本文先介绍一个离线的算法,就做tarjan算法。这个算法是基于并查集和DFS的。Dfs的作用呢,就是递归,一次对树中的每一个节点进行处理。而并查集的作用就是当dfs每访问完(注意,这里是访问完)到一个点的时候,就通过并查集将这个点,和它的子节点链接在一起构成一个集合,也就是将并查集中的pnt值都指向当前节点。这样就把树中的节点分成了若干个的集合,然后就是根据这些集合的情况来对输入数据来进行处理。

      比方说当前访问到的节点是u,等u处理完之后呢,ancestor[u]就构成了u的集合中的点与u点的LCA,而ancestor[fa[u]]就构成了,u的兄弟节点及其兄弟子树的集合中点与u的LCA,而ancestor[fa[fa[u]]]就构成了u的父亲节点的兄弟节点及其兄弟子树的集合中的点与u的LCA。然后依次类推,这样就构成了这个LCA的离线算法。

以上来自 pursuit的专栏

 

首先,Tarjan算法是一种离线算法,也就是说,它要首先读入所有的询问(求一次LCA叫做一次询问),然后并不一定按照原来的顺序处理这些询问。而打乱这个顺序正是这个算法的巧妙之处。看完下文,你便会发现,如果偏要按原来的顺序处理询问,Tarjan算法将无法进行。

   Tarjan算法是利用并查集来实现的。它按DFS的顺序遍历整棵树。对于每个结点x,它进行以下几步操作:
   * 计算当前结点的层号lv[x],并在并查集中建立仅包含x结点的集合,即root[x]:=x。
   * 依次处理与该结点关联的询问。
   * 递归处理x的所有孩子。
   * root[x]:=root[father[x]](对于根结点来说,它的父结点可以任选一个,反正这是最后一步操作了)。

  现在我们来观察正在处理与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)的询问则根本不必存储。  如果在并查集的实现中使用路径压缩等优化措施,一次查询的复杂度将可以认为是常数级的,整个算法也就是线性的了。

上面内容来自NOCOW

 

 

 

 

TarjanLCA的递归过程就是深度优先搜索。
 
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)全部存储,这样就能保证完整性

参考   applesun

 

下面这幅图可以让大家有个感性的认识   BY hnust_xiehonghao

 

 

 

 

下面是一个最基础的LCA题目    http://poj.org/problem?id=1330

赤裸裸的 题意 输入cas 后  有cas组数据 输入 n   再输入n-1 条边    之后输入x y  问x y的最近公共祖先是什么

[cpp]  view plain copy
 
  1. #include<stdio.h>  
  2. #include<vector>  
  3. #include<string.h>  
  4. using namespace std;  
  5. #define Size 11111  //节点个数  
  6.   
  7. vector<int> node[Size],que[Size];  
  8. int n,pare[Size],anse[Size],in[Size],rank[Size];  
  9.   
  10. int vis[Size];  
  11. void init()  
  12. {  
  13.     int i;  
  14.     for(i=1;i<=n;i++)  
  15.     {  
  16.         node[i].clear();  
  17.         que[i].clear();  
  18.         rank[i]=1;  
  19.         pare[i]=i;///   
  20.     }  
  21.     memset(vis,0,sizeof(vis));  
  22.     memset(in,0,sizeof(in));  
  23.     memset(anse,0,sizeof(anse));  
  24.        
  25. }  
  26.   
  27. int find(int nd)//并查集操作  不解释  
  28. {  
  29.     return pare[nd]==nd?nd:pare[nd]=find(pare[nd]);  
  30. }  
  31. int Union(int nd1,int nd2)//并查集操作  不解释  
  32. {  
  33.     int a=find(nd1);  
  34.     int b=find(nd2);  
  35.     if(a==b) return 0;  
  36.     else if(rank[a]<=rank[b])  
  37.     {  
  38.         pare[a]=b;  
  39.         rank[b]+=rank[a];  
  40.     }  
  41.     else   
  42.     {  
  43.         pare[b]=a;  
  44.         rank[a]+=rank[b];  
  45.     }  
  46.     return 1;  
  47.   
  48. }  
  49.   
  50. void LCA(int root)  
  51. {  
  52.     int i,sz;  
  53.     anse[root]=root;//首先自成一个集合  
  54.     sz=node[root].size();  
  55.     for(i=0;i<sz;i++)  
  56.     {  
  57.            LCA(node[root][i]);//递归子树  
  58.            Union(root,node[root][i]);//将子树和root并到一块   
  59.          anse[find(node[root][i])]=root;//修改子树的祖先也指向root  
  60.     }  
  61.     vis[root]=1;  
  62.     sz=que[root].size();  
  63.     for(i=0;i<sz;i++)  
  64.     {  
  65.             if(vis[que[root][i]])  
  66.             {  
  67.                 printf("%d\n",anse[find(que[root][i])]);///root和que[root][i]所表示的值的最近公共祖先  
  68.                 return ;  
  69.             }  
  70.     }  
  71.      return ;  
  72. }  
  73.   
  74. int main()  
  75. {  
  76.     int cas,i;  
  77.     scanf("%d",&cas);  
  78.     while(cas--)  
  79.     {  
  80.         int s,e;  
  81.         scanf("%d",&n);  
  82.         init();  
  83.         for(i=0;i<n-1;i++)  
  84.         {  
  85.             scanf("%d %d",&s,&e);  
  86.             if(s!=e)  
  87.             {  
  88.                 node[s].push_back(e);  
  89.                // node[e].push_back(s);  
  90.                 in[e]++;  
  91.             }  
  92.         }  
  93.         scanf("%d %d",&s,&e);  
  94.         que[s].push_back(e);  
  95.         que[e].push_back(s);  
  96.         for(i=1;i<=n;i++)  if(in[i]==0) break;//寻找根节点  
  97.     //  printf("root=%d\n",i);  
  98.         LCA(i);  
  99.     }  
  100.     return 0;  
  101. }  


 之后来个加强版

http://acm.hdu.edu.cn/showproblem.php?pid=4547  CD操作  hdu4547

思路:

  求出a和b的最近公共祖先,然后分4种情况讨论

  ①. a和b有一个公共祖先c,则用 c时间戳-a的时间戳+1(1步可以直接从c到b)

  ②. a是b的祖先,则只用1步就可以到达b点

  ③. b是a的祖先,则用a的时间戳-b的时间戳

  ④. a和b是同一个点,则答案是0

参考  http://www.cnblogs.com/Griselda/archive/2013/06/05/3119265.html

[cpp]  view plain copy
 
  1. #include<stdio.h>  
  2. #include<vector>  
  3. #include<string.h>  
  4. #include<map>  
  5. #include<math.h>  
  6. #include<string>  
  7. using namespace std;  
  8. #define Size 111111  //节点个数  
  9. struct Query  
  10. {  
  11.     int nd,id;  
  12. }temp;  
  13. struct out  
  14. {  
  15.     int s,e;  
  16. }out[Size];  
  17. vector<int> node[Size];  
  18. vector<struct Query>que[Size];  
  19. int n,m,pare[Size],ance[Size],in[Size],rank[Size],dis[Size],ans[Size],vis[Size];  
  20. map<string,int>mp;  
  21. void init()  
  22. {  
  23.     int i;  
  24.     for(i=1;i<=n;i++)  
  25.     {  
  26.         node[i].clear();  
  27.         que[i].clear();  
  28.         rank[i]=1;  
  29.         pare[i]=i;///  
  30.     }  
  31.     memset(vis,0,sizeof(vis));  
  32.     memset(in,0,sizeof(in));  
  33.     memset(ance,0,sizeof(ance));  
  34.     memset(dis,0,sizeof(dis));  
  35.     mp.clear();  
  36. }  
  37. int aabs(int aa)  
  38. {  
  39.      if(aa>0) return aa;  
  40.      else return -aa;  
  41. }  
  42. int find(int nd)//并查集操作  不解释  
  43. {  
  44.     return pare[nd]==nd?nd:pare[nd]=find(pare[nd]);  
  45. }  
  46. int Union(int nd1,int nd2)//并查集操作  不解释  
  47. {  
  48.     int a=find(nd1);  
  49.     int b=find(nd2);  
  50.     if(a==b) return 0;  
  51.     else if(rank[a]<=rank[b])  
  52.     {  
  53.         pare[a]=b;  
  54.         rank[b]+=rank[a];  
  55.     }  
  56.     else  
  57.     {  
  58.         pare[b]=a;  
  59.         rank[a]+=rank[b];  
  60.     }  
  61.     return 1;  
  62. }  
  63. void LCA(int root,int num)  
  64. {  
  65.     int i,sz;  
  66.     ance[root]=root;//首先自成一个集合  
  67.     dis[root]=num;  
  68.     sz=node[root].size();  
  69.     for(i=0;i<sz;i++)  
  70.     {  
  71.            LCA(node[root][i],num+1);//递归子树  
  72.            Union(root,node[root][i]);//将子树和root并到一块  
  73.          ance[find(node[root][i])]=root;//修改子树的祖先也指向root  
  74.     }  
  75.     vis[root]=1;  
  76.     sz=que[root].size();  
  77.     for(i=0;i<sz;i++)  
  78.     {  
  79.         int nd1,nd2,idx,ancestor;  
  80.         nd1=root;nd2=que[root][i].nd;idx=que[root][i].id;  
  81.             if(vis[nd2])  
  82.             {  
  83.                   ans[idx]=ance[find(nd2)];  
  84.             }  
  85.     }  
  86.      return ;  
  87. }  
  88.   
  89. int main()  
  90. {  
  91.     int cas,i;  
  92.     scanf("%d",&cas);  
  93.     while(cas--)  
  94.     {  
  95.         char  ss[100],ee[100];  
  96.         int s,e,cnt=1;  
  97.         scanf("%d %d",&n,&m);  
  98.         init();  
  99.         for(i=0;i<n-1;i++)  
  100.         {  
  101.             scanf("%s %s",ee,ss);  
  102.             if(mp.find(ss)==mp.end())  
  103.             {  
  104.                  s=cnt;mp[ss]=cnt++;  
  105.             }  
  106.             else s=mp[ss];  
  107.             if(mp.find(ee)==mp.end())  
  108.             {  
  109.                 e=cnt;mp[ee]=cnt++;  
  110.             }  
  111.             else  e=mp[ee];  
  112.             if(s!=e)  
  113.             {  
  114.                 node[s].push_back(e);  
  115.                 in[e]++;  
  116.             }  
  117.         }  
  118.         for(i=0;i<m;i++)  
  119.         {  
  120.            scanf("%s %s",ss,ee);  
  121.            s=mp[ss];e=mp[ee];  
  122.            out[i].s=s;out[i].e=e;  
  123.            temp.nd=e;temp.id=i;  
  124.            que[s].push_back(temp);  
  125.            temp.nd=s;temp.id=i;  
  126.            que[e].push_back(temp);  
  127.         }  
  128.         for(i=1;i<=n;i++)  if(in[i]==0) break;//寻找根节点  
  129.         LCA(i,0);  
  130.         for(i=0;i<m;i++)  
  131.         {  
  132.             if(out[i].s==out[i].e)  
  133.                 printf("0\n");  
  134.             else  
  135.                 if(out[i].s==ans[i])  
  136.                       printf("1\n");  
  137.             else if(out[i].e==ans[i])  
  138.                 printf("%d\n",dis[out[i].s]-dis[ans[i]]);  
  139.             else  
  140.             printf("%d\n",dis[out[i].s]-dis[ans[i]]+1);  
  141.         }  
  142.     }  
  143.     return 0;  
  144. }  


 

 by hnust_xiehonghao

 

 

hdu   2874

http://acm.hdu.edu.cn/showproblem.php?pid=2874

 

题目大意: 给你一个n个节点m条边的森林,再给定q个查询,每次查询森林里两个点的最近距离。n ,m <= 10000,q <= 100万

 

本题和标准的LCA模板应用有了不小的区别   却可以让人更加透彻的看清LCA的思路  而且本题没有必要去求出公共祖先

具体看代码

 

[cpp]  view plain copy
 
    1. #include<stdio.h>  
    2. #include<string.h>  
    3. #include<vector>  
    4. using namespace std;  
    5. #define Size  11111  
    6. struct Edge  
    7. {  
    8.     int y,val;  
    9. }temp;  
    10. struct Query  
    11. {  
    12.     int y,id;  
    13. }mid;  
    14. int pare[Size],ance[Size],vis[Size],dis[Size],rank[Size],ans[1000000+100],n,m,c,tree[Size];  
    15. vector<struct Query>que[Size];  
    16. vector<struct Edge>node[Size];  
    17. void init()  
    18. {  
    19.     int i;  
    20.     for(i=0;i<=n;i++)  
    21.     {  
    22.         vis[i]=0;  
    23.         pare[i]=i;  
    24.         dis[i]=0;  
    25.         rank[i]=1;  
    26.         que[i].clear();  
    27.         node[i].clear();  
    28.     }  
    29.     memset(ans,-1,sizeof(ans));  
    30. }  
    31. int find(int x)  
    32. {  
    33.     return pare[x]==x?x:pare[x]=find(pare[x]);  
    34. }  
    35. /* 
    36. void Union(int x,int y) 
    37. x=find(x); 
    38. y=find(y); 
    39. if(x!=y) 
    40. if(rank[x]>rank[y]) 
    41. rank[x]+=rank[y]; 
    42. pare[y]=x; 
    43. else 
    44. rank[y]+=rank[x]; 
    45. pare[x]=y; 
    46. */  
    47. void LCA(int root,int d,int k)//k表示是以第k个点作为根的树  
    48. {  
    49.     int i,sz,nd1,nd2;  
    50.     vis[root]=1; //已经遍历过的点 要标记一下 不要  
    51.     tree[root]=k;dis[root]=d;  
    52.     // ance[root]=root;  
    53.     sz=node[root].size();  
    54.     for(i=0;i<sz;i++)  
    55.     {  
    56.         nd2=node[root][i].y;  
    57.         if(!vis[nd2])  
    58.         {  
    59.             LCA(nd2,d+node[root][i].val,k);  
    60.             // Union(node[root][i].y,root);//用带rank的幷查集操作答案不对 不知道why  
    61.             int w=find(nd2),m=find(root);  
    62.             if(w!=m)  
    63.             {  
    64.                pare[w]=m;//这样才对  
    65.             }  
    66.             //ance[find(node[root][i].y)]=root;  
    67.         }  
    68.     }  
    69.     sz=que[root].size();  
    70.     for(i=0;i<sz;i++)  
    71.     {  
    72.         nd1=root;  
    73.         nd2=que[root][i].y;  
    74.         if(vis[nd2]&&tree[nd1]==tree[nd2])//如果 nd1 nd2 的跟是同一个点 则是同一棵树上的  
    75.         {  
    76.             ans[que[root][i].id]=dis[nd1]+dis[nd2]-2*dis[find(nd2)];  
    77.         }  
    78.     }  
    79. }  
    80. int main()  
    81. {  
    82.     int i,j,x,y,val;  
    83.     while(scanf("%d %d %d",&n,&m,&c)!=EOF)  
    84.     {  
    85.         init();  
    86.         for(i=0;i<m;i++)  
    87.         {  
    88.             scanf("%d %d %d",&x,&y,&val);  
    89.             if(x!=y)  
    90.             {  
    91.                 temp.y=y;temp.val=val;  
    92.                 node[x].push_back(temp);  
    93.                 temp.y=x;  
    94.                 node[y].push_back(temp);//路是2个方向都可以通行的  
    95.             }  
    96.         }  
    97.         for(i=0;i<c;i++)  
    98.         {  
    99.             scanf("%d %d",&x,&y);  
    100.             mid.id=i;  
    101.             mid.y=y;  
    102.             que[x].push_back(mid);  
    103.             mid.y=x;  
    104.             que[y].push_back(mid);  
    105.         }  
    106.         for(i=1;i<=n;i++)  
    107.         {  
    108.             LCA(i,0,i);//以每一个节点作为根节点去深度搜索  找出每个点作为根的所有最近公共祖先  
    109.         }  
    110.         for(i=0;i<c;i++)  
    111.         {  
    112.             if(ans[i]==-1)  
    113.                 printf("Not connected\n");  
    114.             else  
    115.                 printf("%d\n",ans[i]);  
    116.         }  
    117.     }  
    118.     return 0;  
    119. }  
    120. /*本题给的是一个森林 而不是一颗树,由于在加入边的时候,我们让2个方向都能走 这样就 
    121. 形成了一个强连通的快,  对于这个快来说,不管从快上那点出发 都可以遍历这个快上的所 
    122. 有的点,且相对距离是一样的*/  

 

转自http://blog.csdn.net/hnust_xiehonghao/article/details/9109295

转载于:https://www.cnblogs.com/jszyxw/p/4980492.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值