[图论] LCA(最近公共祖先)Tarjan 离线算法

来源:互联网 发布:帝国cms批量删除文章 编辑:程序博客网 时间:2024/06/05 17:26

很好的参考资料:http://taop.marchtea.com/04.04.html    下面的配图和部分文字转载于此文章

离线算法就是指统一输入后再统一输出,而不是边输入边实时输出。Tarjan算法的复杂度为O(N+Q),Q为询问的次数.

由于是离线算法,所以要保存输入的信息,次序问题。

 

若两个结点u、v分别分布于某节点t 的左右子树,那么此节点 t即为u和v的最近公共祖先。更进一步,考虑到一个节点自己就是LCA的情况,得知:


•若某结点t 是两结点u、v的祖先之一,且这两结点并不分布于该结点t 的一棵子树中,而是分别在结点t 的左子树、右子树中,那么该结点t 即为两结点u、v的最近公共祖先。

这个定理就是Tarjan算法的基础。

2.4、Tarjan算法的应用举例 

引用 此文 中的一个例子。(蓝色字体是我加的说明

i) 访问1的左子树

STEP 1:从根结点1开始,开始访问结点1、2、3(祖先相同的节点在同一个集合)

STEP 2:2的左子树结点3访问完毕   当左子树访问完毕后,这里是坐孩子,要将其祖先节点设为父节点,即ancestor[3]=2, 3,2成为一个集合)

STEP 3:开始访问2的右子树中的结点4、5、6

STEP 4:4的左子树中的结点5访问完毕

STEP 5:开始访问4的右子树的结点6

STEP 6:结点4的左、右子树均访问完毕,故4、5、6中任意两个结点的LCA均为4(当一个顶点的子树全部访问以后并返回该顶点时,才可以查询与该顶点有关的最近公共祖先信息,比如查询 4,5的公共祖先,这里用到了并查集,以4为根节点时,一开始father[4]=4( 一开始定义为-1也可以,只要有个唯一标识就行),4,5先合并,father[4]=5, 后来4,6合并,father[ find(4)] =6 ,即father[5]=6 ,当4,5的最近公共祖先即为 ancestor[ find (5)] =ancestor[6]= 4。)

STEP 7:2的左子树、右子树均访问完毕,故2、3、4、5、6任意两个结点的LCA均为2  (2,3,4,5,6在同一个集合中)

如上所述:进行到此step7,当访问完结点2的左子树(3),和右子树(4、5、6)后,结点2、3、4、5、6这5个结点中,任意两个结点的最近公共祖先均为2。

ii) 访问1的右子树

STEP 8:1的左子树访问完毕,开始访问1的右子树

STEP 9:开始访问1的右子树中的结点7、8

STEP 10

STEP 11

STEP 12:1的右子树中的结点7、8访问完毕

当进行到此step12,访问完1的左子树(2、3、4、5、6),和右子树(7、8)后,结点2、3、4、5、6、7、8这7个结点中任意两个结点的最近公共祖先均为1。

STEP 13:1的左子树、右子树均访问完毕(最后所有的节点都在同一个集合)

通过上述例子,我们能看到,使用此Tarjan算法能解决咱们的LCA问题。

过程总结:

上面的流程很清楚,当一个顶点的子树全部访问完并返回该顶点后,才能对涉及到该顶点的查询进行查询,这时候该顶点和子树有着共同的最近祖先,就是该顶点,也许有疑问,该顶点下面的顶点的最近公共祖先不一定是该顶点,比如上图2顶点下面的5,6,它们的最近公共祖先是4。这就是递归的奇妙之处了,它是从上到下,再从下向上返回,不断向上更新,集合也在不断的合并,最初解决的问题就是4,5,6这样的有叶子节点的子树,当返回4时,看看输入中和4相关的查询有没有,如果查询4,6那肯定是4,这时候还没有涉及到顶点2的相关查询,因为递归还没有返回到2,所以每返回到一个节点(节点就是顶点),就看看输入中有没有关于该节点相关的查询。当根节点的左子树全部访问后,左子树的所有节点和根节点的祖先变为了根节点,那么左子树的所有节点和根节点与右子树的任意节点的最近的公共祖先都为根节点。仔细品味一下,这个算法真的很奇妙。

实现方法:

这样的题目为每个顶点都建立邻接表,即保存与该顶点通过一条边直接相连的所有顶点,图是双向的,加边的时候要加正反两条边。

上面是根据给定的图为每个顶点建立邻接表,还要根据输入为每个顶点建立邻接表,还要记录该查询是第几次查询, 3,5  和 5,3的查询结果是一样的。

并查集使用的很巧妙,需要路径压缩,集合根据搜索返回从小不断扩大,包含在里面所有顶点的祖先也在实时更新,最后所有的顶点的祖先为根。

 

下面是bin神的模板:

POJ 1330

先输入t为测试数据组数,然后输入一个n,表示有n个顶点,接下来n-1行,表明有n-1条边,每行包括两个顶点u ,v ,接下来一行是需要查询的两个顶点u,v,即求u,v的最近公共祖先。本题本组测试数据只涉及到了一条查询。

maxn顶点最多个数,maxq最大查询条数.ans[i]保存第i次输入的查询的结果,i从0开始.

#include <iostream>#include <stdio.h>#include <algorithm>#include <string.h>using namespace std;const int maxn=10010;//顶点数const int maxq=100;//最多查询次数,根据题目而定,本题中其实每组数据只有一个查询.//并查集int f[maxn];//根节点int find(int x){    if(f[x]==-1)        return x;    return f[x]=find(f[x]);}void unite(int u,int v){    int x=find(u);    int y=find(v);    if(x!=y)        f[x]=y;}//并查集结束bool vis[maxn];//节点是否访问int ancestor[maxn];//节点i的祖先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 ans[maxn*2];//存储每次查询的结果,下表0~Q-1,其实应该开maxq大小的。int h[maxn],tt;int Q;//题目中需要查询的次数void addquery(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;//相当于两次查询,比如查询  3,5 和5,3结果是一样的,以3为头节点的邻接表中有5,以5为头节点的邻接表中有3    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,0,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)//和顶点u相关的顶点    {        int v=edge[i].to;        if(vis[v])            continue;        LCA(v);        unite(u,v);        ancestor[find(u)]=u;//将u的左右孩子的祖先设为u    }    for(int i=h[u];i!=-1;i=query[i].next)//看输入的查询里面有没有和u节点相关的    {        int v=query[i].q;        if(vis[v])            ans[query[i].index]=ancestor[find(v)];    }}bool flag[maxn];//用来确定根节点的int t;int n,u,v;int main(){    int a,b,c;    scanf("%d(%d):%d",&a,&b,&c);    cout<<a<<b<<c;    scanf("%d",&t);    while(t--)    {        scanf("%d",&n);        init();        memset(flag,0,sizeof(flag));        for(int i=1;i<n;i++)        {            scanf("%d%d",&u,&v);            flag[v]=true;//有入度            addedge(u,v);            addedge(v,u);        }        Q=1;//题目中只有一组查询        for(int i=0;i<Q;i++)        {            scanf("%d%d",&u,&v);            addquery(u,v,i);        }        int root;        for(int i=1;i<=n;i++)        {            if(!flag[i])            {                root=i;                break;            }        }        LCA(root);        for(int i=0;i<Q;i++)            printf("%d\n",ans[i]);    }    return 0;}


 

 

 

 

 

0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 百度网盘密码忘了怎么办 空调冷凝水无法排出怎么办 转账时名字错了怎么办 打款名字错了怎么办 转账名字写错了怎么办 国际汇款汇错了怎么办 汇款英文写错了怎么办 体重秤不显示了怎么办 在ur试完衣服怎么办 汽车主机没有倒车检测线怎么办 合格考补考没过怎么办 合同一式两份双方都丢失怎么办 自控力差的人怎么办 孩子缺乏自控能力该怎么办 名片上换了号码怎么办 格力空调显示u8怎么办 格力空调出现u8怎么办 双肺多发斑点影怎么办 外文翻译没5000字怎么办 睡出永久睡痕怎么办 英语不好学学英文软件怎么办 遥控汽车只能原地打转怎么办 铝合金门上的胶带纸撕不掉怎么办 纸胶带撕不下来怎么办 拼多多卖不出去怎么办 联想键盘被锁了怎么办 台式电脑打不开机怎么办 文件名中不能用特殊符号怎么办 高铁喷雾扣留后怎么办 高铁没收的东西怎么办 安检被收的东西怎么办 我的律师骗我怎么办 没婆婆生了小孩怎么办 没人帮你带孩子怎么办 亲戚在家里不走怎么办 穷人家的孩子该怎么办 空腹吃李子胃疼怎么办 情侣空间农场谷仓空间不够怎么办 王者荣耀情侣解除对方不同意怎么办 oppo手机进了水怎么办 淘宝卖号被骗了怎么办