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

很好的参考资料: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;
}
时间: 2024-08-05 07:09:35

[图论] LCA(最近公共祖先)Tarjan 离线算法的相关文章

笔记:LCA最近公共祖先 Tarjan(离线)算法

LCA最近公共祖先 Tarjan他贱(离线)算法的基本思路及其算法实现 本文是网络资料整理或部分转载或部分原创,参考文章如下: https://www.cnblogs.com/JVxie/p/4854719.html http://blog.csdn.net/ywcpig/article/details/52336496 https://baike.baidu.com/item/最近公共祖先/8918834?fr=aladdin 最近公共祖先简称LCA(Lowest Common Ancesto

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

学习博客:  http://noalgo.info/476.html 讲的很清楚! 对于一颗树,dfs遍历时,先向下遍历,并且用并查集维护当前节点和父节点的集合.这样如果关于当前节点(A)的关联节点(B)(及要求的最近祖先的另一个点)之前被访问过,那么 B可定已经属于一个集合,先前对于访问过的点,已经维护了那个点所在集合的根,所以找到B节点所在集合的根,那么这个点就是最近的根,因为对于dfs访问的顺序.

最近公共祖先 tarjan离线算法 C++

最近做到一道题目,大概的意思就是求一个多叉树中两个节点的最近公共祖先,输入是用邻接矩阵表示的. 要想理解tarjan算法并实现它,需要先理解一下内容: 1) 深度优先搜索:tarjan算法核心思想:当某节点刚刚搜索完毕时,看与其相关的结点v是否已经被访问,如果v已经被访问过了,则它们的最近公共祖先就是v的祖先. 2) 并查集原理和实现方法,并查集的代表和祖先的区别(其实也可以一起表示),祖先的更新时刻 3) 如何表示多叉数(邻接链表,邻接矩阵),如何表示查询对,如何记录查询结果 下面是c++实现

POJ 1330 Nearest Common Ancestors(最近公共祖先 Tarjan离线)

题目链接:http://poj.org/problem?id=1330 题目: Description A rooted tree is a well-known data structure in computer science and engineering. An example is shown below:  In the figure, each node is labeled with an integer from {1, 2,...,16}. Node 8 is the ro

求LCA最近公共祖先的离线Tarjan算法_C++

最近一直在刷算法,过几天再来写详细的思路 先丢个模板,这个是用双链树存的 1 #include<algorithm> 2 #include<iostream> 3 #include<cstdlib> 4 #include<cstring> 5 #include<cstdio> 6 #include<cmath> 7 #include<stack> 8 #define N 100001 9 using namespace s

[笔记]LCA最近公共祖先---倍增在线算法

059M37853N虏3Jhttp://www.zcool.com.cn/collection/ZMTg2OTM5ODg=.html 痹o83RI世9EUS两http://www.zcool.com.cn/collection/ZMTg2OTQwMTY=.html 猩骋05K型51抡MChttp://www.zcool.com.cn/collection/ZMTg2OTQxMjg=.html 4辣腾膛且j匠9坝3凳W1http://www.zcool.com.cn/collection/ZMTg

LCA(最近公共祖先)--tarjan离线算法 hdu 2586

HDU 2586 How far away ? Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Submission(s): 11320    Accepted Submission(s): 4119 Problem Description There are n houses in the village and some bidirectional roads c

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

一.梳理概念 定义:对于有根树T的两个结点u.v,最近公共祖先LCA(T,u,v)表示一个结点x,满足x是u.v的祖先且x的深度尽可能大. 通俗地讲,最近公共祖先节点,就是两个节点在这棵树上深度最大的公共的祖先节点,即两个点在这棵树上距离最近的公共祖先节点. 提示:父亲节点也是祖先节点,节点本身也是它的祖先节点. 给出一棵树,如图所示: 由上面的定义可知:3和5的最近公共祖先为1,5和6的最近公共祖先为2,2和7的最近公共祖先为2, 6和7的最近公共祖先为4. 二.繁文缛节 注意注意注意!!!尚

POJ 1330 LCA最近公共祖先 离线tarjan算法

题意要求一棵树上,两个点的最近公共祖先 即LCA 现学了一下LCA-Tarjan算法,还挺好理解的,这是个离线的算法,先把询问存贮起来,在一遍dfs过程中,找到了对应的询问点,即可输出 原理用了并查集和dfs染色,先dfs到底层开始往上回溯,边并查集合并 一边染色,这样只要询问的两个点均被染色了,就可以输出当前并查集的最高父亲一定是LCA,因为我是从底层层层往上DSU和染色的,要么没被染色,被染色之后,肯定就是当前节点是最近的 #include <iostream> #include <

HDU 2586 How Far Away?(Tarjan离线算法求lca)

题意:给定一棵树n个节点m个询问,每次询问两个节点之间的距离. 思路:Tarjan离线算法求lca. 这题一开始交了n发一直爆栈.......百度了一下大概说的是这样hdu用的是windows服务器所以栈大小极其坑爹,稍微深一点的递归就会爆栈(正式比赛一般不会爆) 解决方法就是加一句#pragma comment(linker, "/STACK:1024000000,1024000000") 用c++交就好.....当然这只是针对比较坑爹oj来说的取巧的方法 #include<c