Tarjan离线算法求最近公共祖先(LCA)

转载注明出处:http://blog.csdn.net/u011400953

Tarjan离线算法求LCA介绍

     前言:首先,本人搞懂Tarjan求最近公共祖先(LCA),也是浏览了大量其他网友大牛的文章,若是看了本文仍未弄懂的,可以尝试自己做一下模板题(裸题)HDU2586,自己用数据去感受一下,或者可以换篇文章再看,或许他的文章更对你的“胃口”。

 

        一:概念介绍

            1:最近公共祖先

           对于有根树Tree的两个结点u、v,其最近公共祖先LCA表示一个结点x,满足x是u、v的祖先且x的深度尽可能大。另一种理 解方式是把Tree理解为一个无向无环图,而其最近公共祖先LCA即u到v的最短路上深度最小的点。

            2:并查集:详见http://baike.baidu.com/view/521705.htm

            3:离线算法:

            算法设计策略都是基于在执行算法前输入数据已知的基本假设,也就是说,对于一个离线算法,在开始时就需要知道问题的所有输入数据,而且在解决一个问题后就要立即输出结果,通常将这类具有问题完全信息前提下设计出的算法成为离线算法( off line algorithms)

 

        二:Tarjan离线算法求LCA

           首先根据LCA的定义,我们可以想到一种最简单的方法来求u,v的LCA:分别从u,v开始向根节点走,当这两个点走到第一个相同的节点时,这个节点就是LCA(u,v)

           但是这样求效率不够,求一次的最坏的时间复杂度就是O(N),若是再多来几个询问就会超时

           现在介绍一种当询问次数为Q,节点数为N时时间复杂度为O(N+Q)的离线求LCA的算法:Tarjan算法

           这种算法是基于DFS和并查集来实现的。设fa[x]为x的父亲,dist[x]为x节点到根节点的距离。首先从一号根节点(记为u)开始访问他的每一个子节点(记为v),并用根节点与当前访问的子节点的距离更新dist值,即dist[v]=dist[u]+map[v][u],其中map[v][u]表示v到u的距离,然后将当前子节点当做根点用上述同样步骤递归下去,并在递归回溯后将其fa[v]值更新,这样的目的是保证子节点v的所有子树全部被访问过。

           现在我们需知道第k个询问的LCA是什么,那么这个操作应在询问中两个节点的子树全部访问完的基础上再进行。对于现在状态的根点u,访问它的子节点v,若v点“作过”根点,即被递归过,才能保证v的所有子树被全部访问完,这时才能将与之有关的询问<即询问中包含v点>更新其LCA=get(v),get为并查集,即找到v所在集合的起点,因为并查集在这里的作用就是将同一子树中的子节点的父亲指向该子树的根节点,相当于归为了一个集合,这个集合的起点就是当前子树的根节点。

           这样,从1号根节点出发,向下递归直到到达叶子节点位置,树中每个节点都被访问过了一次,在回溯后,为了对每个询问(记总询问次数为Q)更新LCA值访问了相关点,则时间复杂度为O(N)+O(Q),因此这是个O(N+Q)的算法!

          实现代码:

          这里实现是用临接链表存的根与子节点的关系,b数组中存的是询问信息,a数组存的是边的信息,下面的代码为Tarjan的主函数,若没看懂数组的含义,请接着看下面例题HDU2586

[cpp]  view plain copy
  1. void Tarjan(int x)  
  2. {  
  3.       fa[x]=x;//作为当前的根节点,将其父亲指向自己   
  4.       vis[x]=true;//标记该点已经走过   
  5.       for(int i=question[x];i;i=b[i].next)   
  6.       {  
  7.             int now_to=b[i].to;  
  8.             if(vis[now_to])//如果其子节点及子节点的子树全部访问完才会进入这一步,由vis判断             
  9.                   LCA[b[i].num]=get(now_to);//更新LCA值   
  10.       }  
  11.       for(int i=edge[x];i;i=a[i].next)  
  12.       {  
  13.             int now_to=a[i].to;  
  14.             if(!vis[now_to])  
  15.             {  
  16.                   dist[now_to]=dist[x]+a[i].v;//更新dist值   
  17.                   Tarjan(now_to);//相当于将now_to当成根节点递归下去   
  18.                   fa[now_to]=x;//更新子节点的父亲  
  19.             }  
  20.       }   
  21. }  


 

        三:例题分析:HDU 2586 《How far away ?》

      题目大意】:

          一个村子里有n个房子,这n个房子用n-1条路连接起来,接下来有m次询问,每次询问两个房子a,b之间的距离是多少。

       【分析】:

         这是个求最近公共祖先的问题,用临接链表存下每条边和询问的信息,然后跑一遍Tarjan,最后对于询问i,j的LCA,答案为dist[i]+dist[j]-2*dist[LCA(i,j)],这里dist[i]为i点到1号根节点的距离

       【代码】:用时31MS,好像将更新LCA操作整体放后面也行

[cpp]  view plain copy
  1. #include<stdio.h>  
  2. #include<string.h>  
  3. #include<stdlib.h>  
  4. #include<algorithm>  
  5. #include<iostream>  
  6. #include<vector>  
  7. #include<stack>  
  8. #include<queue>  
  9. using namespace std;  
  10. #define MAXEDGE 100001  
  11. #define MAXN 40001  
  12. #define MAXM 410   
  13. struct EDGE{int to,v,next;};  
  14. struct QUESTION{int to,num,next;};  
  15. EDGE a[MAXEDGE];  
  16. QUESTION b[MAXM];  
  17. int DATA,N,M,edge[MAXN],question[MAXN],tot1=0,tot2=0,ansf[MAXM],anst[MAXM];  
  18. int fa[MAXN],LCA[MAXM],dist[MAXN];//dist[i]记录了i点到根节点的距离   
  19. bool vis[MAXN];  
  20. void add_edge(int x,int y,int value)  
  21. {  
  22.       a[++tot1].to=y;//这条边的到达点   
  23.       a[tot1].v=value;//这条边的长度   
  24.       a[tot1].next=edge[x];//与这条边有相同初始点的上一条边编号   
  25.       edge[x]=tot1;//更新编号   
  26. }  
  27. void add_question(int x,int y,int number)  
  28. {  
  29.       b[++tot2].to=y;//这个询问的点标号   
  30.       b[tot2].num=number;//这个询问的对应编号   
  31.       b[tot2].next=question[x];  
  32.       question[x]=tot2;  
  33. }  
  34. int get(int x)  
  35. {  
  36.       if(fa[x]==x)  return x;  
  37.       return fa[x]=get(fa[x]);  
  38. }  
  39. void Tarjan(int x)  
  40. {  
  41.       fa[x]=x;//作为当前的根节点,将其父亲指向自己   
  42.       vis[x]=true;//标记该点已经走过   
  43.       for(int i=question[x];i;i=b[i].next)   
  44.       {  
  45.             int now_to=b[i].to;  
  46.             if(vis[now_to])//如果其子节点及子节点的子树全部访问完才会进入这一步,由vis判断             
  47.                   LCA[b[i].num]=get(now_to);//更新LCA值   
  48.       }  
  49.       for(int i=edge[x];i;i=a[i].next)  
  50.       {  
  51.             int now_to=a[i].to;  
  52.             if(!vis[now_to])  
  53.             {  
  54.                   dist[now_to]=dist[x]+a[i].v;//更新dist值   
  55.                   Tarjan(now_to);//相当于将now_to当成根节点递归下去   
  56.                   fa[now_to]=x;//更新子节点的父亲  
  57.             }  
  58.       }   
  59. }  
  60. int main()  
  61. {  
  62.       //freopen("input.in","r",stdin);  
  63.       //freopen("output.out","w",stdout);   
  64.       scanf("%d",&DATA);  
  65.       for(int now=1;now<=DATA;now++)  
  66.       {  
  67.             memset(vis,false,sizeof(vis));  
  68.             memset(edge,0,sizeof(edge));  
  69.             memset(question,0,sizeof(question));  
  70.             memset(a,0,sizeof(a));  
  71.             memset(b,0,sizeof(b));  
  72.             memset(dist,0,sizeof(dist));  
  73.             memset(fa,0,sizeof(fa));  
  74.             tot1=0,tot2=0;  
  75.             scanf("%d%d",&N,&M);  
  76.             for(int i=1;i<=N;i++)  
  77.                   fa[i]=i;//首先将所有节点的父亲节点指向自己   
  78.             for(int i=1;i<=N-1;i++)  
  79.             {  
  80.                   int A,B,C;  
  81.                   scanf("%d%d%d",&A,&B,&C);   
  82.                   add_edge(A,B,C);//建无向边,用临接链表存储   
  83.                   add_edge(B,A,C);  
  84.             }  
  85.             for(int i=1;i<=M;i++)  
  86.             {  
  87.                   int A,B;  
  88.                   scanf("%d%d",&A,&B);  
  89.                   add_question(A,B,i);//这里用进行两次记录,保证在更新LCA时能顾全这两个点   
  90.                   add_question(B,A,i);  
  91.                   ansf[i]=A,anst[i]=B;  
  92.             }  
  93.             Tarjan(1);  
  94.             for(int i=1;i<=M;i++)  
  95.                   printf("%d\n",dist[ansf[i]]+dist[anst[i]]-2*dist[LCA[i]]);  
  96.       }  
  97.       //system("pause");  
  98.       return 0;  
  99. }  


 


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值