zoukankan      html  css  js  c++  java
  • 算法详解(LCA&RMQ&tarjan)补坑啦!完结撒花(。◕ˇ∀ˇ◕)

    首先,众所周知,求LCA共有3种算法(树剖就不说了,太高级,以后再学。。)。

    1、树上倍增(ST表优化)

    2、RMQ&时间戳(ST表优化)

    3、tarjan(离线算法)不讲。。(后面补坑啦!)

    一、树上倍增

    这种方法原理是这样的:

    我们可以知道,最朴素的算法就是一步一步的并查集往上找,知道找到2个点并查集相同,即为LCA

    但这种算法的时间效率为O(NM)看到0<n,m<5*10^5我们就知道一定会炸。

    但是,我们可以发现给出树后,每个点的LCA及走到LCA的路径一定是固定的。

    所以可以ST表优化。

    首先先BFS出每个点在树上的深度。。(记为depth[i])

    接着我们要先让2个点的深度相同,之后2个点一起走,可以加快效率。

    最后我们直接倍增上去。(if(fa[i][u]!=fa[i][v])u=fa[i][u];v=fa[i][v];)

    最后只要往上走一步,就是LCA了。

    重点!倍增时倍增循环在外!不能在内!否则会挂!

    下面贴代码(debug了三小时,才调好。膜拜zxyer)

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    using namespace std;
    struct data{
        int next,to;
    }g[500011];
    int depth[500011];
    int que[1000001];
    bool visit[500011];
    int head[500011];
    int fa[30][500011];
    int n,q,root,num=0;
    int lca(int u,int v){
        if(depth[u]<depth[v])swap(u,v);
        int dc=depth[u]-depth[v];
        for(int i=0;i<=29;i++)
        if((1<<i)&dc&&fa[i][u]){
        u=fa[i][u];    
        }
        if(u==v)return u;
        for(int i=29;i>=0;i--)
        if(fa[i][u]!=fa[i][v]&&fa[i][u]){
        u=fa[i][u];
        v=fa[i][v];    
        }
        return fa[0][u];
    }
    void bfs(int u)
    {
        memset(que,0,sizeof(que));
        memset(visit,0,sizeof(visit));
        visit[u]=true;
        que[1]=u;
        int h=1,l=1;
        while(h<=l)
        {
            int rt=que[h];
            for (int i=head[rt];i;i=g[i].next)
            {
                int v=g[i].to;
                if ( !visit[v] )
                 {
                        visit[v]=true;
                        depth[v]=depth[rt]+1;
                        que[++l]=v;
                 }
            }
            h++;   
        }
    }
    int main(){
        scanf("%d%d",&n,&q);
        memset(g,0,sizeof(g));
        memset(fa,0,sizeof(fa));
        memset(depth,0,sizeof(depth));
        for(int i=1;i<n;i++)
        {
        int f,t;
        scanf("%d%d",&f,&t);
        g[++num].next=head[f];
        head[f]=num;
        g[num].to=t;
        fa[0][t]=f;
        if(fa[0][f]==0)root=f;
        }
        for(int j=1;j<=29;j++)
        for(int i=1;i<=n;i++)
        {
            
            fa[j][i]=fa[j-1][fa[j-1][i]];
        }
        depth[root]=1;
        bfs(root);
        for(int i=1;i<=q;i++){
        {
            int num1,num2;
            scanf("%d%d",&num1,&num2);
            printf("%d
    ",lca(num1,num2));
        }    
        }
    }

    二、RMQ+时间戳

    这个算法理解了很久才懂

    首先我们都知道如果把树看成一个无向图,那么LCA一定在u->v的最短路上。而且,LCA的点就是最短路上depth最小的点。

    换句话说,就是LCA是2个点到根节点的路径的第一个交汇处。

    接下来用dfs为点标号,用id[i]表示这个点第一次出现在顶点序列的标号。

    接下来就是求id[u]<i<id[v]中depth的最小值啦!

    这个过程可以用RMQ高速解决。

    所以LCA=RMQ(depth)(u,v)

    注意!这个算法比较难懂,可以先看看RMQ,理解之后再画画图,恩,就差不多了。

    下面贴代码

     #include <cstdio>

    #include <cstring>  
    #include <queue>  
    #include <algorithm>  
    #define MAXN 1010  
    #define MAXM 100000  
    using namespace std;  
    struct Edge  
    {  
        int from, to, next;  
    };  
    Edge edge[MAXM];  
    int head[MAXN], edgenum;  
    int vs[MAXN<<1];//第i次DFS访问节点的编号  
    int depth[MAXN<<1];//第i次DFS访问节点的深度  
    int id[MAXN];//id[i] 记录在vs数组里面 i节点第一次出现的下标  
    int dfs_clock;//时间戳  
    int N, M, Q;//点数 边数 查询数  
    int dp[MAXN<<1][20];//dp[i][j]存储depth数组  以下标i开始的,长度为2^j的区间里 最小值所对应的下标  
    void init()  
    {  
        edgenum = 0;  
        memset(head, -1, sizeof(head));  
    }  
    void addEdge(int u, int v)  
    {  
        Edge E = {u, v, head[u]};  
        edge[edgenum] = E;  
        head[u] = edgenum++;  
    }  
    void getMap()  
    {  
        int a, b;  
        while(M--)  
            scanf("%d%d", &a, &b),  
            addEdge(a, b), addEdge(b, a);  
    }  
    void DFS(int u, int fa, int d)//当前遍历点以及它的父节点  遍历点深度  
    {  
        id[u] = dfs_clock;  
        vs[dfs_clock] = u;  
        depth[dfs_clock++] = d;  
        for(int i = head[u]; i != -1; i = edge[i].next)  
        {  
            int v = edge[i].to;  
            if(v == fa) continue;  
            DFS(v, u, d+1);  
            vs[dfs_clock] = u;//类似 回溯  
            depth[dfs_clock++] = d;  
        }  
    }  
    void find_depth()  
    {  
        dfs_clock = 1;  
        memset(vs, 0, sizeof(vs));  
        memset(id, 0, sizeof(id));  
        memset(depth, 0, sizeof(depth));  
        DFS(1, -1, 0);//遍历  
    }  
    void RMQ_init(int NN)//预处理 区间最小值  
    {  
        for(int i = 1; i <= NN; i++)  
            dp[i][0] = i;  
        for(int j = 1; (1<<j) <= NN; j++)  
        {  
            for(int i = 1; i + (1<<j) - 1 <= NN; i++)  
            {  
                int a = dp[i][j-1];  
                int b = dp[i + (1<<(j-1))][j-1];  
                if(depth[a] <= depth[b])  
                    dp[i][j] = a;  
                else  
                    dp[i][j] = b;  
            }  
        }  
    }  
    int query(int L, int R)  
    {  
        //查询L <= i <= R 里面使得depth[i]最小的值 返回对应下标  
        int k = 0;  
        while((1<<(k+1)) <= R-L+1) k++;  
        int a = dp[L][k];  
        int b = dp[R - (1<<k) + 1][k];  
        if(depth[a] <= depth[b])  
            return a;  
        else  
            return b;  
    }  
    int LCA(int u, int v)  
    {  
        int x = id[u];//比较大小 小的当作左区间 大的当作右区间  
        int y = id[v];  
        if(x > y)  
            return vs[query(y, x)];  
        else  
            return vs[query(x, y)];  
    }  
    void solve()  
    {  
        int a, b;  
        while(Q--)  
        {  
            scanf("%d%d", &a, &b);  
            printf("LCA(%d %d) = %d
    ", a, b, LCA(a, b));  
        }  
    }  
    int main()  
    {  
        while(scanf("%d%d%d", &N, &M, &Q) != EOF)  
        {  
            init();  
            getMap();  
            find_depth();//DFS遍历整个树 求出所需要的信息  
            RMQ_init(dfs_clock - 1);  
            solve();  
        }  
        return 0;  
    }  

    重点同上哈!倍增写外面!

     三、tarjan算法求LCA

    这个算法和之前的tarjan求强联通分量有一些差别(只是名字一样而已)

    这个算法非常妙啊,其实就是从根节点dfs标记父节点,但是厉害的地方在于,在它标记完根节点后,恰好能够更新答案。

    这个算法比较绕,网上有很多的图解,我就不贴了,我们来具体算法理解。

    下面是tarjan的主体,h就是链表的头。。这个不讲,这里讲一下q数组,它用于存储询问,g[i].to表示他要查询的第二个节点。

    void tarjan(int x){
        for(int i=h[x];i;i=g[i].next)
        tarjan(g[i].to),f[g[i].to]=x;
        for(int i=q[x];i;i=g[i].next)
        ans[g[i].to]=ans[g[i].to]>0?getfa(ans[g[i].to]):x;     
    }

    很显然,在回溯后,我们计算答案,如果没有出现g[i].to的答案,我们就将他标记为x,这是什么意思呢?其实如果标记为X,就说明在dfs时先搜到了x,再搜g[i].to,显然x即为这2者的公共祖先。

    但是如果我们已经更新过ans[g[i].to],其实这说明了两个点不在同一子树上,所以我们要找到后一个节点的父亲。(重点来了!)

    我们在做getfather的操作的时候,由于是先序遍历,所以我们做到这个dfs时,寻找父亲只会找到他们的公共祖先然后停止。是不是很妙?

    嗯对,所以是不是很简单?

    #include<iostream>
    #include<cstdio>
    using namespace std;
    int h[500005],q[500005];
    bool fa[500005];
    int f[500005];
    int ans[500005];
    int n,m,num=0;
    struct edge{
        int to,next;
    }g[1500005];
    int getfa(int x){return f[x]?f[x]=getfa(f[x]):x;}
    void ins(int *h,int u,int v){g[++num].next=h[u];h[u]=num;g[num].to=v;}
    void tarjan(int x){
        for(int i=h[x];i;i=g[i].next)
        tarjan(g[i].to),f[g[i].to]=x;
        for(int i=q[x];i;i=g[i].next)
        ans[g[i].to]=ans[g[i].to]>0?getfa(ans[g[i].to]):x;     
    }
    int main(){
        scanf("%d%d",&n,&m);
        for(int i=1;i<n;i++){
            int x,y;
            scanf("%d%d",&x,&y);
            ins(h,x,y);    
            fa[y]=1;
        }
        for(int i=0;i<m;i++)
        {
            int x,y;
            scanf("%d%d",&x,&y);
            ins(q,x,i);ins(q,y,i);
        }
        for(int i=1;i<=n;i++)if(!fa[i]){tarjan(i);break;}
        for(int i=0;i<m;i++)printf("%d
    ",ans[i]);
        return 0;
    }
  • 相关阅读:
    Torch7学习笔记(四)StochasticGradient
    Torch7学习笔记(三)Sequencialization
    Torch7学习笔记(二)nn Package
    Torch7学习笔记(一)CmdLine
    java.lang.IllegalArgumentException: pointerIndex out of range错误
    android.view.ViewRoot$CalledFromWrongThreadException的解决办法
    红米手机 拍照 崩溃
    java.lang.IllegalArgumentException: Illegal character in query at index xxx:
    Android EditText得到和失去焦点时,自定义处理内容
    Android动态设置Marggin属性
  • 原文地址:https://www.cnblogs.com/ghostfly233/p/6835850.html
Copyright © 2011-2022 走看看