LCA的离线算法

LCA(Least Common Ancestor),顾名思义,是指在一棵树中,距离两个点最近的两者的公共节点。也就是说,在两个点通往根的道路上,肯定会有公共的节点,我们就是要求找到公共的节点中深度尽量深的点。还可以表示成另一种说法,就是如果把树看成是一个图,这找到这两个点中的最短距离。Tarjan作为离线off-line算法,在程序开始前,需要将所有等待询问的节点对提前存储(非常重要,也是为什么称之为“离线”的原因),然后程序从树根开始执行LCA()。

如图:


下面我将详细解释LCA一般用法:

(1)建立树的表示

程序设计中,用vector<int> node[n]来保存树的结构

  1.  for(i=0;i<n-1;i++)  
  2.         {  
  3.             scanf("%d %d",&s,&e);    /*s,e分别为树上的节点*/
  4.             if(s!=e)  
  5.             {  
  6.                 node[s].push_back(e);  
  7.                 in[e]++;       /*in[n] 节点的入度,目的是找根节点*/
  8.         }  
 上图执行完这段程序后,结果如图所示:{注:节点的编号和数组下标一一对应}


红色的数字是node[s].size不为0的大小.(整棵树的表示有了哦),是计算出来的哈,写上去是为了便于大家理解。

(2)保存查询,用vector<int> que[n]来保存所要的查询对。

  1. querynum=某个数;
  2. while(querynum!=0)
  3. scanf("%d %d",&s,&e);  
  4.         que[s].push_back(e);  
  5.         que[e].push_back(s);
  6.         querynum--;
  7. }  
如:分别输入(1,2),(2,4),(2,3),(2,7),(7,8),(5,6)等等等等,也就是你可以输入任意一对树上的节点,你需要查询它们的最近公共祖先。

执行后,que如图下图所示:


注意:红色的数字是为了便于大家理解,计算出来的que[n].size。大家是否注意到图中(1,2),(2,4),(2,3),(2,7),(7,8),(5,6)又分别存成了(2,1),(4,2),(3,2),(7,2),(8,7),(6,5)。这是因为根据LCA离线算法,上面提到的询问(x,y)中,y是已处理过的结点。那么,如果y尚未处理怎么办?所以,只要在查询列表中加入两个询问(x, y)、(y,x),那么就可以保证这两个询问有且仅有一个被处理了(暂时无法处理的那个就pass掉)。

(3)LCA算法

假设为(u,v),u为此时已经搜完的子树的根节点,v的位置就只有两种可能,一种是在u的子树内,另一种就是在其之外。

对于在u的子树内的话,最近公共祖先肯定是可以直接得出为u;

对于在u的子树之外的v,我们已经将v搜过了,且已经知道了v的祖先,那么我们可以根据dfs的思想,v肯定是和u在一颗子树下的,而且这颗子树是使得他们能在一颗子树下面深度最深的。而这个子树的根节点刚好就是v的并查集所保存的祖先。所以对于这种情况的(u,v),它们的最近公共祖先就是v的并查集祖先。关于并查集,这篇博客说的很清楚


  1. int find(int u)//并查集,获取nd的父亲节点
  2. {  
  3.     return pare[u]==u?u:pare[u]=find(pare[u]);  
  4. }  
  5. int Union(int u,int v)//并查集操作,将子树节点和根节点合并在一起
  6. {  
  7.     int a=find(u);  
  8.     int b=find(v);  
  9.     if(a==b) return 0;  
  10.     else if(rank[a]<=rank[b])  
  11.     {  
  12.         pare[a]=b;  
  13.         rank[b]+=rank[a];  
  14.     }  
  15.     else   
  16.     {  
  17.         pare[b]=a;  
  18.         rank[a]+=rank[b];  
  19.     }  
  20.     return 1;  
  21.   
  22. }  
             通过pare[]这个数组,可以找到当前节点的根节点。

最后:

  1.   
  2. void LCA(int root)  
  3. {  
  4.     int i,sz;  
  5.     anse[root]=root;//首先自成一个集合  
  6.     sz=node[root].size();  
  7.     for(i=0;i<sz;i++)  
  8.     {  
  9.            LCA(node[root][i]);//递归子树  
  10.            Union(node[root][i],root);//将子树和root并到一块   
  11.            anse[find(node[root][i])]=root;//修改子树的祖先也指向root  
  12.     }  
  13.     vis[root]=1;  
  14.     sz=que[root].size();  
  15.     for(i=0;i<sz;i++)  
  16.     {  
  17.             if(vis[que[root][i]])  
  18.             {  
  19.                 printf("%d\n",anse[find(que[root][i])]);///root和que[root][i]所表示的值的最近公共祖先  
  20.                 return ;  
  21.             }  
  22.     }  
  23.      return ;  
根据TarjanLCA的实现算法可以看出,只有当某一棵子树全部遍历处理完成后,才将该子树的根节点标记为已访问(vis[root]=1),假设程序按上面的树形结构进行遍历,首先从节点1开始,然后递归处理根为2的子树,然后递归处理根为5的子树,当子树处理完后,节点5标记为已访问(vis[5]=1),处理和5相关的查询(见程序LCA中的14行,节点2未被访问,跳过),返回,将(5,2)合并,pare[5]=2,修改子树的祖先也指向root。以此类推,当子树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)。   

建议:大家亲自走一遍程序。

代码:

  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],querynum;  
  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 u)//并查集,获取u的父亲节点
  28. {  
  29.     return pare[u]==u?u:pare[u]=find(pare[u]);  
  30. }  
  31. int Union(int u,int v)//并查集操作,将子树节点和根节点合并在一起
  32. {  
  33.     int a=find(u);  
  34.     int b=find(v);  
  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. void LCA(int root)  
  50. {  
  51.     int i,sz;  
  52.     anse[root]=root;//首先自成一个集合  
  53.     sz=node[root].size();  
  54.     for(i=0;i<sz;i++)  
  55.     {  
  56.            LCA(node[root][i]);//递归子树  
  57.            Union(node[root][i],root);//将子树和root并到一块   
  58.          anse[find(node[root][i])]=root;//修改子树的祖先也指向root  
  59.     }  
  60.     vis[root]=1;  
  61.     sz=que[root].size();  
  62.     for(i=0;i<sz;i++)  
  63.     {  
  64.             if(vis[que[root][i]])  
  65.             {  
  66.                 printf("%d\n",anse[find(que[root][i])]);///root和que[root][i]所表示的值的最近公共祖先  
  67.                 return ;  
  68.             }  
  69.     }  
  70.      return ;  
  71. }  
  72.   
  73. int main()  
  74. {  
  75.         int i;
  76.         scanf("%d",&n,&querynum);  
  77.         init();  
  78.         for(i=0;i<n-1;i++)  
  79.         {  
  80.             scanf("%d %d",&s,&e);  
  81.             if(s!=e)  
  82.             {  
  83.                 node[s].push_back(e);  
  84.                 in[e]++;  
  85.             }  
  86.         }  
  87.        
  88. int find(int u)//并查集,获取nd的父亲节点
  89. {  
  90.     return pare[u]==u?u:pare[u]=find(pare[u]);  
  91. }  
  92. int Union(int u,int v)//并查集操作,将子树节点和根节点合并在一起
  93. {  
  94.     int a=find(u);  
  95.     int b=find(v);  
  96.     if(a==b) return 0;  
  97.     else if(rank[a]<=rank[b])  
  98.     {  
  99.         pare[a]=b;  
  100.         rank[b]+=rank[a];  
  101.     }  
  102.     else   
  103.     {  
  104.         pare[b]=a;  
  105.         rank[a]+=rank[b];  
  106.     }  
  107.     return 1;  
  108.   

    1. while(querynum!=0)
    2. scanf("%d %d",&s,&e);  
    3.         que[s].push_back(e);  
    4.         que[e].push_back(s);
    5.         querynum--;
    6. }  
  109.        
  110.         for(i=1;i<=n;i++)  if(in[i]==0) break;//寻找根节点  
  111.         LCA(i);  
  112.     }  
  113.     return 0;  
  114. }  

  • int find(int u)//并查集,获取nd的父亲节点
  • {  
  •     return pare[u]==u?u:pare[u]=find(pare[u]);  
  • }  
  • int Union(int u,int v)//并查集操作,将子树节点和根节点合并在一起
  • {  
  •     int a=find(u);  
  •     int b=find(v);  
  •     if(a==b) return 0;  
  •     else if(rank[a]<=rank[b])  
  •     {  
  •         pare[a]=b;  
  •         rank[b]+=rank[a];  
  •     }  
  •     else   
  •     {  
  •         pare[b]=a;  
  •         rank[a]+=rank[b];  
  •     }  
  •     return 1;  
  •   
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值