zoukankan      html  css  js  c++  java
  • Tarjan求LCA

    2020-03-14

    发布于摸鱼酱的个人博客


    首先,Tarjan算法是一个离线求LCA的算法,与倍增不同。

    倍增的做法是(O(n))预处理,(O(log_2n))回答每一次询问(倍增),也就是说可以每输入一次询问就立刻计算输出,是在线算法。

    Tarjan呢,则是先读入完所有的数据,再用一次(dfs)遍历之后全部输出,在读入完之前不能计算出答案,是离线算法。


    接下来就是具体的过程。

    先假设有这样一棵树。

    对于这棵树,我们再假装有这样几组询问:

    (LCA(4,6))

    (LCA(2,10))

    (LCA(1,6))

    首先我们将这几组询问分别存储到对应的点上。假如第(i)组询问需要求(LCA(u,v)),为了让从u,v都能够访问到这组询问,我们就以每一个点编号为下标,开一个存放{目标点,询问编号}的vector,就像这样.

    struct node//定义部分
    {
        int v,id;
    };
    vector<node> ask[500010];
    
    ...
    //对于每组询问u,v
    ask[u].push_back((node){v,i});
    ask[v].push_back((node){u,i});
    ...
    

    为了更清晰,在示例图中我们就不记录询问编号。记录完成后如下图:

    树2.jpg

    这棵询问树完成之后,输入也就完成了,接下来就是离线求LCA的环节了。

    在这个环节,我们采用dfs对其进行一次遍历,但是过程中会穿插来自并查集的get求祖先操作。初始化即每个点都是自己的父结点。

    贴一下get操作,后面比较好理解。

    int get(int x)
    {
    	return fa[x]==x?x:fa[x]=get(fa[x]);
    	/*或者写成下面的形式
    	if(fa[x]==x)return x;
    	else return fa[x]=get(fa[x]);
    	*/
    }
    

    我们把目前经历过但是还没回溯的点标记为红色R(无实际意义,为了看得更清晰),回溯了的点标记为蓝色B。是否遍历过就用一个bool数组来统计。

    以下是具体操作过程,不需要可以跳过。


    • 从根节点0出发,先到达1.发现结点1有询问LCA(1,6),查询发现结点6还未遍历,忽略

    树3.jpg


    • 从1出发,到达2,记录发现结点2有询问LCA(2,10),查询结点10还未遍历,忽略。

    • 2没有子节点了,标记2已经访问,回溯到父节点1记录fa[2]=1

    树4.jpg


    • 从1出发,到达3,记录fa[3]=1,结点3没有询问,继续遍历。

    树5.jpg


    • 从3出发,分别到达4,5,结点4的询问点没有出现,忽略并回溯;结点5没有询问,回溯。记录fa[4]=fa[5]=3

    树6.jpg


    • 从3出发,到达6,记录fa[6]=3,分别处理结点6的两个询问。

    • 对于LCA(6,4),只需要记录结点4的祖先(通过get),fa[4]=3->fa[3]=3->停止,所以LCA(6,4)的答案即为3,记录在对应答案编号下。

    • 对于LCA(6,1),同样求结点1的祖先,因为1还没有回溯,所以fa[1]=1->停止,记录1到LCA(6,1)下。

    • 结点6,3,1回溯,记录fa[6]=3,fa[3]=1,fa[1]=0,回溯到结点0.

    • 从0出发,遍历结点7,遍历结点8,结点8回溯,记录fa[8]=7......

    • 其余同理。


    好了,下拉到这里就可以了

    下面是全过程展示,黄边为已经确定父子结点关系的边。

    全过程.gif


    相信你已经理解了为什么要把一个询问分成两次记录下来,因为这两次里面虽然只会有一次有效,但是我们不能知道哪一次是有效的,所以就都记录下来,方便判断。

    至于原理...首先我们在LCA(u,v)的询问中,答案肯定是v的祖先,而深度最小的那个,即v的祖先中第一个还没有回溯的结点f。u一定是由f往下递归得到的。可以多看几次GIF演示来想想是为什么。


    最近公共祖先(模板题)

    参考代码如下:

    #include<bits/stdc++.h>
    using namespace std;
    struct edge
    {
        int to,next;
    }e[1000010];
    int head[1000010],cnt;
    int fa[500010];
    bool vis[500010];
    int ans[500010];
    struct node
    {
        int v,id;
    };
    vector<node> ask[500010];
    void add(int u,int v)
    {
        e[++cnt]=(edge){v,head[u]};
        head[u]=cnt;
    }
    int get(int x)
    {
        return fa[x]==x?x:fa[x]=get(fa[x]);
    }
    void dfs(int u,int f)
    {
        for(int i=head[u];i;i=e[i].next)
            if(e[i].to!=f)dfs(e[i].to,u),fa[e[i].to]=u;
        int len=ask[u].size();
        for(int i=0;i<len;i++)
            if(vis[ask[u][i].v])ans[ask[u][i].id]=get(ask[u][i].v);
        vis[u]=1;
    }
    int main()
    {
        int n,m,s;
        cin>>n>>m>>s;
        for(int i=1;i<=500000;i++)
            fa[i]=i;
        for(int i=1;i<n;i++)
        {
            int u,v;
            scanf("%d%d",&u,&v);
            add(u,v);
            add(v,u);
        }
        for(int i=1;i<=m;i++)
        {
            int u,v;
            scanf("%d%d",&u,&v);
            ask[u].push_back((node){v,i});
            ask[v].push_back((node){u,i});
        }
        dfs(s,0);
        for(int i=1;i<=m;i++)
            printf("%d
    ",ans[i]);
        return 0;
    }
    
  • 相关阅读:
    JAVA Rest High Level Client如何取聚合后的数据
    elasticsearch中TermQuery查不到数据问题
    项目中redis改brpop阻塞模式为订阅模式的实现(二)
    项目中redis改brpop阻塞模式为订阅模式的实现(一)
    《算法笔记》5. 前缀树、桶排序、排序算法总结
    《算法笔记》4. 堆与堆排序、比较器详解
    《算法笔记》3. 归并排序、随机快排整理
    《算法笔记》2. 链表、栈、队列、递归、哈希表、顺序表
    《算法笔记》1. 复杂度、排序、二分、异或
    深入理解Java线程状态转移
  • 原文地址:https://www.cnblogs.com/moyujiang/p/12505094.html
Copyright © 2011-2022 走看看