最近公共祖先LCA:Tarjan算法(介绍1)

本文转自:http://kmplayer.iteye.com/blog/604518

1,并查集+dfs 
对整个树进行深度优先遍历,并在遍历的过程中不断地把一些目前可能查询到的并且结果相同的节点用并查集合并. 

2,分类,使每个结点都落到某个类中,到时候只要执行集合查询,就可以知道结点的LCA了。 
对于一个结点u.类别有: 
以u为根的子树、除类一以外的以f(u)为根的子树、除前两类以外的以f(f(u))为根的子树、除前三类以外的以f(f(f(u)))为根的子树…… 

类一的LCA为u,类二为f(u),类三为f(f(u)),类四为f(f(f(u)))。这样的分类看起来好像并不困难。 
但关键是查询是二维的,并没有一个确定的u。接下来就是这个算法的巧妙之处了。 
利用递归的LCA过程。 
当lca(u)执行完毕后,以u为根的子树已经全部并为了一个集合。而一个lca的内部实际上做了的事就是对其子结点,依此调用lca. 
当v1(第一个子结点)被lca,正在处理v2的时候,以v1为根的子树+u同在一个集合里,f(u)+编号比u小的u的兄弟的子树 同在一个集合里,f(f(u)) + 编号比f(u)小的 f(u)的兄弟 的子树 同在一个集合里……  
而这些集合,对于v2的LCA都是不同的。因此只要查询x在哪一个集合里,就能知道LCA(v2,x) 

还有一种可能,x不在任何集合里。当他是v2的儿子,v3,v4等子树或编号比u大的u的兄弟的子树(等等)时,就会发生这种情况。即还没有被处理。还没有处理过的怎么办?把一个查询(x1,x2)往查询列表里添加两次,一次添加到x1的列表里,一次添加到x2的列表里,如果在做x1的时候发现 x2已经被处理了,那就接受这个询问。(两次中必定只有一次询问被接受). 

3,应用: http://acm.pku.edu.cn/JudgeOnline/problem?id=1330 
实现代码: 
Cpp代码   收藏代码
  1. #include<iostream>  
  2. #include<vector>  
  3. using namespace std;  
  4.   
  5. const int MAX=10001;  
  6. int f[MAX];  
  7. int r[MAX];  
  8. int indegree[MAX];//保存每个节点的入度  
  9. int visit[MAX];  
  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;  
  20.         f[i]=i;  
  21.         indegree[i]=0;  
  22.         visit[i]=0;  
  23.         ancestor[i]=0;  
  24.         tree[i].clear();  
  25.         Qes[i].clear();  
  26.     }  
  27.   
  28. }  
  29.   
  30. int find(int 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  
  52.     {  
  53.         f[b]=a;  
  54.         r[a]+=r[b];  
  55.     }  
  56.     return 1;  
  57.   
  58. }//合并函数,如果属于同一分支则返回0,成功合并返回1  
  59.   
  60.   
  61. void LCA(int u)  
  62. {  
  63.     ancestor[u]=u;  
  64.     int size = tree[u].size();  
  65.     for(int i=0;i<size;i++)  
  66.     {  
  67.         LCA(tree[u][i]);  
  68.         Union(u,tree[u][i]);  
  69.         ancestor[find(u)]=u;  
  70.     }  
  71.     visit[u]=1;  
  72.     size = Qes[u].size();  
  73.     for(int i=0;i<size;i++)  
  74.     {  
  75.         //如果已经访问了问题节点,就可以返回结果了.  
  76.         if(visit[Qes[u][i]]==1)  
  77.         {  
  78.             cout<<ancestor[find(Qes[u][i])]<<endl;  
  79.             return;  
  80.         }  
  81.     }  
  82. }  
  83.   
  84.   
  85. int main()  
  86. {  
  87.     int cnt;  
  88.     int n;  
  89.     cin>>cnt;  
  90.     while(cnt--)  
  91.     {  
  92.         cin>>n;;  
  93.         init(n);  
  94.         int s,t;  
  95.         for(int i=1;i<n;i++)  
  96.         {  
  97.             cin>>s>>t;  
  98.             tree[s].push_back(t);  
  99.             indegree[t]++;  
  100.         }  
  101.         //这里可以输入多组询问  
  102.         cin>>s>>t;  
  103.         //相当于询问两次  
  104.         Qes[s].push_back(t);  
  105.         Qes[t].push_back(s);  
  106.         for(int i=1;i<=n;i++)  
  107.         {  
  108.             //寻找根节点  
  109.             if(indegree[i]==0)  
  110.             {  
  111.                 LCA(i);  
  112.                 break;  
  113.             }  
  114.         }  
  115.     }  
  116.     return 0;  
  117. }  

#########################################################################################

第一次写最近公共祖先问题,用的邻接表指针。

对于一棵有根树,就会有父亲结点,祖先结点,当然最近公共祖先就是这两个点所有的祖先结点中深度最大的一个结点。

       0

       |

       1

     /   \

   2      3

比如说在这里,如果0为根的话,那么1是2和3的父亲结点,0是1的父亲结点,0和1都是2和3的公共祖先结点,但是1才是最近的公共祖先结点,或者说1是2和3的所有祖先结点中距离根结点最远的祖先结点。

在求解最近公共祖先为问题上,用到的是Tarjan的思想,从根结点开始形成一棵深搜树,非常好的处理技巧就是在回溯到结点u的时候,u的子树已经遍历,这时候才把u结点放入合并集合中,这样u结点和所有u的子树中的结点的最近公共祖先就是u了,u和还未遍历的所有u的兄弟结点及子树中的最近公共祖先就是u的父亲结点。以此类推。。这样我们在对树深度遍历的时候就很自然的将树中的结点分成若干的集合,两个集合中的所属不同集合的任意一对顶点的公共祖先都是相同的,也就是说这两个集合的最近公共最先只有一个。对于每个集合而言可以用并查集来优化,时间复杂度就大大降低了,为O(n + q),n为总结点数,q为询问结点对数。

另外Tarjan解法,是一个离线算法,就是说它必须将所有询问先记录下来,再一次性的求出每个点对的最近公共祖先,只有这样才可以达到降低时间复杂度。另外还有一个在线算法,有待学习,呵呵。。

复制代码
//parent为并查集,FIND为并查集的查找操作
//QUERY为询问结点对集合
//TREE为基图有根树
Tarjan(u)
visit[u] = true
for each (u, v) in QUERY
if visit[v]
ans(u, v) = FIND(v)
for each (u, v) in TREE
if !visit[v]
Tarjan(v)
parent[v] = u
复制代码

 

hdu2586 How far away ?

这道题题意是,给定一棵树,每条边都有一定的权值,q次询问,每次询问某两点间的距离。这样就可以用LCA来解,首先找到u, v 两点的lca,然后计算一下距离值就可以了。这里的计算方法是,记下根结点到任意一点的距离dis[],这样ans = dis[u] + dis[v] - 2 * dis[lca(v, v)]了,这个表达式还是比较容易理解的。。

#########################################################################################




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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值