码迷,mamicode.com
首页 > 编程语言 > 详细

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

时间:2017-07-26 01:38:27      阅读:490      评论:0      收藏:0      [点我收藏+]

标签:优先   init   在线   深度优先   不清楚   搜索   基本   href   har   

一、梳理概念

定义:对于有根树T的两个结点u、v,最近公共祖先LCA(T,u,v)表示一个结点x,满足x是u、v的祖先且x的深度尽可能大。

通俗地讲,最近公共祖先节点,就是两个节点在这棵树上深度最大的公共的祖先节点,即两个点在这棵树上距离最近的公共祖先节点。

提示:父亲节点也是祖先节点,节点本身也是它的祖先节点。

给出一棵树,如图所示:

技术分享

由上面的定义可知:3和5的最近公共祖先为1,5和6的最近公共祖先为2,2和7的最近公共祖先为2, 6和7的最近公共祖先为4。

 

二、繁文缛节

注意注意注意!!!尚不清楚二叉树的后序遍历、并查集、tarjan算法的童鞋可以不要往下看了。

【解决LCA问题——Tarjan 离线算法】

简介:利用并查集在一次DFS(深度优先遍历)中完成所有询问。换句话说,要所有询问都读入后才开始计算,所以是一种离线的算法。

二叉树后序遍历的性质:当两个节点(u,v)的最近公共祖先是x时,进行后序遍历的时候,必然先访问完x的所有子树(其中包含u、v),然后才会返回到x所在的节点。这个性质就是我们使用Tarjan算法解决最近公共祖先问题的核心思想。

用图讲算法:

技术分享

如上图所示,找出根节点root到节点u的关键路径P(解释一下,关键路径P内容为root-->…-->p2-->p1-->u),已遍历的点位于路径P中某个节点(譬如说p2)的子树中(因为上面几行的后序遍历性质说明了后访问子树根节点)。第一种情况:当遍历到u时v已遍历过(u的子树已遍历完,因为u为子树根节点),那么v必然存在于子树pk中(也可以在u的子树中),此时LCA(u,v)就等于现在v所在集合的祖先pk(嘿嘿,你画个图就能看明白,因为u是路径P上最后一个根节点,如果v在u的子树中,那么最近公共祖先就是u);第二种情况:如果v还没有遍历到,则继续遍历,只不过LCA(u,v)要等到遍历到v时才能知道了,原理如上。需要注意的一点是,为了保持上图的性质,如果一个节点的一个子树遍历完了,需要合并该节点的子树集合。

 

三、算法描述

首先我们看一下都有哪些求公共最近祖先的算法并分析一下这些算法的性能,在这里我们设询问次数为q(询问次数是什么?往下读你会懂的):

  • 暴力(实际做题不可行):对于每个询问,遍历所有的点,时间复杂度为O(n*q)。
  • Tarjan(离线)算法: 在一次遍历中把所有询问一次性解决,时间复杂度是O(n+q)。
  • 倍增(在线)算法:留坑。

【Tarjan算法过程模拟】

首先,上图:

技术分享

 

条件:我们需要寻找最近公共祖先的点对为<3,5>,<5,6>,<2,7>,<6,7>。

初始化:①开一个pre数组,记录父亲节点,初始化pre[i]=i;②再开一个vis数组,记录节点是否已经访问,初始化 memset(vis,0,sizeof(vis))。

过程:

  1. 先取1为根节点, 发现其有两个子节点2和3,先搜索2(后序遍历基本操作),又发现2有两个子节点4和5,先搜索4,4也有两个子节点6和7,先搜索6,这时发现6没有子节点了,然后寻找与其有询问关系的节点(条件中的点对供参考),发现5和7均与6有询问关系,但都没被访问过。所以返回并标记vis[6]=1,pre[6]=4;
  2. 接着搜索7,发现7没有子节点,然后寻找与其有询问关系的节点,发现6与其有询问关系,且vis[6]=1,所以LCA(6,7)=find(6)=4。结束并标记vis[7]=1,pre[7]=4;
  3. 现在节点4已经搜完,且没有与其有询问关系的节点,vis[4]=1,pre[4]=2;
  4. 搜索5,发现其有子节点8,搜索8,发现8没有子节点,然后寻找与其有询问关系的节点,也没有,于是返回,且vis[5]=1,pre[8]=5;
  5. 节点5已经搜完,发现有两个与其有询问关系的节点6和7,且vis[6]=1,所以LCA(5,6)=find(6)=2;因为vis[7]=1,所以LCA(5,7)=find(7)=2;遍历完毕返回,标记vis[5]=1,pre[5]=2;(find过程:pre[7]=4-->pre[4]=2  ==》2,很标准的一个并查集查找代表元操作)
  6. 节点2已经搜完,发现有一个与其有询问关系的节点7,且vis[7]=1,故LCA(2,7)=find(7)=2。遍历完毕,标记vis[2]=1,pre[2]=1;
  7. 接着搜索3,没有子节点,发现有一个与其有询问关系的节点5,因为vis[5]=1,所以LCA(3,5)=find(5)=1;遍历结束,标记vis[3]=1,pre[3]=1;(find过程:pre[5]=2-->pre[2]=1  ==》1)
  8. 这时返回到了节点1,它没有与之有询问关系的点了,且其pre[1]=1,搜索结束。

至此,完成求最小公共祖先的操作。

 

四、代码模板

由于LCA的题目千变万化,下面给出最基础的模板(给出一系列边用邻接表保存,把询问也用邻接表保存,只求LCA,不维护其他值)

void Tarjan(int now)
{
    vis[now]=1;
    for(int i=head1[now];i!=-1;i=e1[i].next)
    {
        int t=e1[i].t;
        if(vis[t]==0)
        {
            Tarjan(t);
            join(now,t);
        }
    }
    for(int i=head2[now];i!=-1;i=e2[i].next)
    {
        int t=e2[i].t;
        if(vis[t]==1)
        {
            e2[i].lca=find(t);
            e2[i^1].lca=e2[i].lca;
        }
    }
}

 

五、沙场练兵

题目:POJ 1470

代码:

/* 
 * POJ 1470 
 * 给出一颗有向树,Q个查询 
 * 输出查询结果中每个点出现次数 
 */ 
/* 
 * LCA离线算法,Tarjan 
 * 复杂度O(n+Q); 
 */ 
const int MAXN = 1010; 
const int MAXQ = 500010;//查询数的最大值 
 
//并查集部分 
int F[MAXN];//需要初始化为-1 
int find(int x) 
{ 
  if(F[x] == -1)return x; 
  return F[x] = find(F[x]); 
} 
void bing(int u,int v) 
{ 
  int t1 = find(u); 
  int t2 = find(v); 
  if(t1 != t2) 
    F[t1] = t2; 
} 
//************************ 
bool vis[MAXN];//访问标记 
int ancestor[MAXN];//祖先 
struct Edge 
{ 
  int to,next; 
}edge[MAXN*2]; 
int head[MAXN],tot; 
void addedge(int u,int v) 
{ 
  edge[tot].to = v; 
  edge[tot].next = head[u]; 
  head[u] = tot++; 
} 
struct Query 
{ 
  int q,next; 
  int index;//查询编号 
}query[MAXQ*2]; 
int answer[MAXQ];//存储最后的查询结果,下标0~Q-1 
int h[MAXQ]; 
int tt; 
int Q; 
 
void add_query(int u,int v,int index) 
{ 
  query[tt].q = v; 
  query[tt].next = h[u]; 
  query[tt].index = index; 
  h[u] = tt++; 
  query[tt].q = u; 
  query[tt].next = h[v]; 
  query[tt].index = index; 
  h[v] = tt++; 
} 
 
void init() 
{ 
  tot = 0; 
  memset(head,-1,sizeof(head)); 
  tt = 0; 
  memset(h,-1,sizeof(h)); 
  memset(vis,false,sizeof(vis)); 
  memset(F,-1,sizeof(F)); 
  memset(ancestor,0,sizeof(ancestor)); 
} 
 
void LCA(int u) 
{ 
  ancestor[u] = u; 
  vis[u] = true; 
  for(int i = head[u];i != -1;i = edge[i].next) 
  { 
    int v = edge[i].to; 
    if(vis[v])continue; 
    LCA(v); 
    bing(u,v); 
    ancestor[find(u)] = u; 
  } 
  for(int i = h[u];i != -1;i = query[i].next) 
  { 
    int v = query[i].q; 
    if(vis[v]) 
    { 
      answer[query[i].index] = ancestor[find(v)]; 
    } 
  } 
} 
 
bool flag[MAXN]; 
int Count_num[MAXN];
int main() 
{ 
    int n; 
  int u,v,k; 
  while(scanf("%d",&n) == 1) 
  { 
    init(); 
    memset(flag,false,sizeof(flag)); 
    for(int i = 1;i <= n;i++) 
    { 
      scanf("%d:(%d)",&u,&k); 
      while(k--) 
      { 
        scanf("%d",&v); 
        flag[v] = true; 
        addedge(u,v); 
        addedge(v,u); 
      } 
    } 
    scanf("%d",&Q); 
    for(int i = 0;i < Q;i++) 
    { 
      char ch; 
      cin>>ch; 
      scanf("%d %d)",&u,&v); 
      add_query(u,v,i); 
    } 
    int root; 
    for(int i = 1;i <= n;i++) 
      if(!flag[i]) 
      { 
        root = i; 
        break; 
      } 
    LCA(root); 
    memset(Count_num,0,sizeof(Count_num)); 
    for(int i = 0;i < Q;i++) 
      Count_num[answer[i]]++; 
    for(int i = 1;i <= n;i++) 
      if(Count_num[i] > 0) 
        printf("%d:%d\n",i,Count_num[i]); 
  } 
    return 0; 
} 

 

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

标签:优先   init   在线   深度优先   不清楚   搜索   基本   href   har   

原文地址:http://www.cnblogs.com/xzxl/p/7237125.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!