最近公共祖先算法(LCA)

http://blog.csdn.net/taotaotaotao910429/article/details/7746650


发现网上对此算法真是多之又多,看了几个小时才算看懂。

 

写下我的理解思路,首先,LCA要用到并查集和深度优先搜索,其中并查集用来查找和合并各个节点集合,深度优先搜索用了搜索问题节点是否在同一个集合中。其实就是递归。(1):其中递的过程:首先算法从根开始,对每一棵子树进行深度优先搜索,访问根时,将创建由根结点构建的集合,然后把根节点的祖先设为自身,然后遍历该节点的每个子节点,也就是该节点的其他子树,如果子树是多层就选子节点重复上述过程,直到叶子节点。(2)归的过程:从叶子节点开始,找到其父节点,然后和父节点的集合合并,并把其祖先设为父节点,直到归到根节点。注意,在这过程中要判断问题节点是否在同一集合中,比如节点u,节点v,如果v在集合u中,那么他们最近公共祖先就应该是u,如果v不在u中,则遍历v时进行判断,自然就是v的最近祖先是v和u的最近公共祖先。

 

1.这个算法基于并查集和深度优先搜索。算法从根开始,对每一棵子树进行深度优先搜索,访问根时,将创建由根结点构建的集合,然后对以他的孩子结点为根的子树进行搜索,使对于 u, v 属于其某一棵子树的 LCA 询问完成。这时将其所有子树结点与根结点合并为一个集合。 对于属于这个集合的结点 u, v 其 LCA 必定是根结点。

2对于最近公共祖先问题,我们先来看这样一个性质,当两个节点(u,v)的最近公共祖先是x时,那么我们可以确定的说,当进行后序遍历的时候,必然先访问完x的所有子树,然后才会返回到x所在的节点。这个性质就是我们使用Tarjan算法解决最近公共祖先问题的核心思想。

      同时我们会想这个怎么能够保证是最近的公共祖先呢?我们这样看,因为我们是逐渐向上回溯的,所以我们每次访问完某个节点x的一棵子树,我们就将该子树所有节点放进该节点x所在的集合,并且我们设置这个集合所有元素的祖先是该节点x。那么到我们完成对一个节点的所有子树的访问时,我们将这个节点标记为已经找到了祖先的点。

       这个时候就体现了Tarjan采用离线的方式解决最近公共祖先的问题特点所在了,所以这个时候就体现了这一点。假设我们刚刚已经完成访问的节点是a,那么我们看与其一同被询问的另外一个点b是否已经被访问过了,若已经被访问过了,那么这个时候最近公共祖先必然是b所在集合对应的祖先c,因为我们对a的访问就是从最近公共祖先c转过来的,并且在从c的子树b转向a的时候,我们已经将b的祖先置为了c,同时这个c也是a的祖先,那么c必然是a、b的最近公共祖先。

       对于一棵子树所有节点,祖先都是该子树的根节点,所以我们在回溯的时候,时常要更新整个子树的祖先,为了方便处理,我们使用并查集维护一个集合的祖先。总的时间复杂度是O(n+q)的,因为dfs是O(n)的,然后对于询问的处理大概就是O(q)的。

从网上找了这样一个容易理解算法的代码:http://blog.csdn.net/lixiandejian/article/details/6661074

  1. <pre class="cpp" name="code">#include<iostream>    
  2. #include<vector>    
  3. using namespace std;    
  4.   
  5. const int MAX=17;    
  6. int f[MAX];             //每个节点所属集合?  
  7. int r[MAX];             //r是rank(秩) 合并  
  8. int indegree[MAX];      //保存每个节点的入度    
  9. int visit[MAX];         //只有0和1,表示某节点id是否已处理完毕  
  10. vector<int> tree[MAX],Qes[MAX];  //树,查询  
  11. int ancestor[MAX];      //祖先集合  
  12.   
  13.   
  14. void init(int n)    
  15. {    
  16.     for(int i=1;i<=n;i++)    
  17.     {    
  18.   
  19.         r[i]=1;                 //每个节点的初始秩为1,秩的初始化也很重要,不初始化也可以,省去了计算每个集合秩的开销  
  20.         f[i]=i;                 //每个节点的父节点初始为自身?  
  21.         indegree[i]=0;    
  22.         visit[i]=0;    
  23.         ancestor[i]=0;          //祖先为0  
  24.         tree[i].clear();    
  25.         Qes[i].clear();    
  26.     }    
  27.   
  28. }    
  29.   
  30. int find(int n)                 //查找n节点所在的集合  
  31. {    
  32.     if(f[n]==n)    
  33.         return n;    
  34.     else    
  35.         f[n]=find(f[n]);    
  36.     return f[n];    
  37. }//查找函数,并压缩路径    
  38.   
  39. int Union(int x,int y)    
  40. {    
  41.     int a=find(x);    
  42.     int b=find(y);    
  43.     if(a==b)    
  44.         return 0;    
  45.     //相等的话,x向y合并    
  46.     else if (r[a] < r[b])  
  47.     {    
  48.         f[a] = b;  
  49.         r[b] += r[a];           //小的秩合并向大的秩  
  50.     }    
  51.     else  if(r[a] == r[b])      //两秩相等,合并到左边的秩  
  52.     {    
  53.         f[b] = a;  
  54.         r[a] += r[b];   
  55.     }  
  56.     else  
  57.     {  
  58.         f[b] = a;  
  59.         r[a] += r[b];  
  60.     }  
  61.     return 1;    
  62.   
  63. }//合并函数,如果属于同一分支则返回0,成功合并返回1    
  64.   
  65.   
  66. void LCA(int u)    
  67. {    
  68.     ancestor[u]=u;    
  69.     int size = tree[u].size();    
  70.     for(int i=0;i<size;i++)    
  71.     {    
  72.         LCA(tree[u][i]);    
  73.         Union(u,tree[u][i]);    
  74.         ancestor[find(u)]=u;        //让u的父节点祖先为u,因为是回溯操作,一定能保证集合的祖先是最近祖先  
  75.     }    
  76.     visit[u]=1;    
  77.     size = Qes[u].size();             
  78.     for(int i=0;i<size;i++)    
  79.     {    
  80.         //如果已经访问了问题节点,就可以返回结果了.    
  81.         if(visit[Qes[u][i]]==1)    
  82.         {    
  83.             cout<<ancestor[find(Qes[u][i])]<<endl;          //如果这个点处理过,那么这个祖先就是共同祖先  
  84.             //          return;    
  85.             continue;  
  86.         }    
  87.     }    
  88. }    
  89.   
  90.   
  91. int main()    
  92. {    
  93.     int n = 16;    
  94.     init(n);            //数的总节点数  
  95.     int s,t;    
  96.   
  97.     //先构造树  
  98.     tree[8].push_back(5);indegree[5]++;  
  99.     tree[8].push_back(4);indegree[4]++;  
  100.     tree[8].push_back(1);indegree[1]++;                 //对节点ID为8的节点添加3个子节点,相应的子节点增加入度  
  101.   
  102.     tree[5].push_back(9);indegree[9]++;  
  103.   
  104.     tree[4].push_back(6);indegree[6]++;  
  105.     tree[4].push_back(10);indegree[10]++;  
  106.   
  107.     tree[1].push_back(14);indegree[14]++;  
  108.     tree[1].push_back(13);indegree[13]++;  
  109.   
  110.     tree[6].push_back(15);indegree[15]++;  
  111.     tree[6].push_back(7);indegree[7]++;  
  112.   
  113.     tree[10].push_back(11);indegree[11]++;  
  114.     tree[10].push_back(16);indegree[16]++;  
  115.     tree[10].push_back(2);indegree[2]++;  
  116.   
  117.     tree[16].push_back(3);indegree[3]++;  
  118.     tree[16].push_back(12);indegree[12]++;  
  119.   
  120.     //输入查询  
  121.     cin>>s>>t;    
  122.     //相当于询问两次,如果t在s的左边,那么在遍历完s时将无法得出结果    
  123.     Qes[s].push_back(t);  
  124.     Qes[t].push_back(s);  
  125.   
  126.     for(int i=1;i<=n;i++)    
  127.     {    
  128.         //寻找根节点    
  129.         if(indegree[i]==0)          //根节点的入度为0  
  130.         {    
  131.             LCA(i);    
  132.             break;    
  133.         }    
  134.     }    
  135.     return 0;    
  136. }  </pre><br>  
  137. <br>  
  138. <p></p>  
  139. <p></p>  
  140. 算法步骤:1 由跟节点开始,进行深度优先遍历,遍历到叶子节点,置其对应的visit[i] = 1; 2 将父节点与子节点进行合并(将他们置于同一个集合,详细看union代码),然后,将集合的祖先置为当前节点。(要注意回溯的过程,一定是保证高层次的) 3 询问查询,即qes[u][i],u是查询节点之一(正好是当前节点),i是另一个查询节点,如果i此时已被处理完毕,说明i在u的左边,那么i所在集合(并不是像有些文章说的i的父节点,这概念不正确)的祖先一定u,i的最近共同祖先(如果u,i在同一子树,则很好理解,如果u,i在不同子树,那么i所在集合的祖先也是u的祖先(注意回溯,上升,下降));如果i此时未被处理,说明i在u的右边,对于u,i的询问只能跳过,但是对i,u的询问可以处理。  
  141.  这个是两个节点共同祖先的查询,多个的话就用前两个的查询结果与下一个组成一个查询,依次类推。  
  142. <p></p>  
  143. 参考问题链接:http://poj.org/problem?id=1330参考代码链接:http://kmplayer.iteye.com/blog/604518 (此代码结果是正确的,但代码的一些概念不太正确)参考知识链接:http://my.chinaunix.net/space.php?uid=1721137&do=blog&id=181005 ; http://hi.baidu.com/%B1%B1%BE%A9%CE%D2%B0%AE%C4%E3/blog/item/aaa01dc630e1940a9d163d0b.html感谢相关文章作者!  
  144. <pre></pre>  

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值