• NOIP模板复习(2) LCA的三种解法


    NOIP模板复习(2) LCA的三种解法

    LCA还是图论中蛮重要的部分,解法众多,这里只拿三个比较常用的板子出来说说

    目录

    1.树上倍增

    1.1算法原理

    1.2算法实现

    2.Tarjan算法

    2.1算法原理

    2.2算法实现

    3.RMQ实现

    3.1算法原理

    3.2算法实现

    4.总解


    1.树上倍增

      树上倍增,顾名思义是利用了倍增的思想实现的在线的LCA算法,具体来讲就是利用(2^i)可以相加组成任何数的原理实现的。


    1.1算法原理

      倍增算法主要是利用(2^i)可以相加组成任何数这一性质来组织信息转移状态,具体就是维护一个倍增数组(f[i][j])表示编号为(i)的节点向上跳(2^j)步所到达的点。通过简单的思考我们可以发现节点(i)向上跳(2^j)步的节点是(i)号节点向上跳(2^{j-1})步的节点再向上跳(2^{j-1})步所到达的节点,因此我们可以得到一个式子:(f[i][j]=f[f[i][j-1]][j-1]),通过这个式子,我们可以轻易的在很短的时间内推出倍增数组。

      而有了倍增数组就好办了,我们可以利用dfs求出每个节点距根节点的深度,然后我们便可以先将两个节点上跳到同一高度(注:以后上跳都可以利用倍增数组完成),再将两个节点同时上跳。当两个节点的祖先都相同时,便找到了两个节点的最近公共祖先。

      该算法单个查询的时间复杂度为(O(log(n)))


    1.2算法实现

    代码如下
    
    #include <iostream>
    #include <cstdio>
    #include <cmath>
    #include <vector>
    #include <cstdlib>
    #include <cstring>
    #include <algorithm>
    using namespace std;
    vector<int> tree[1005];
    int deep[1005];
    int anc[1005][25];
    int father[1005];
    void dfs(int root)//预处理出倍增数组和深度
    {
        anc[root][0]=father[root];
        for(int i=1;i<20;i++)
        {
            anc[root][i]=anc[anc[root][i-1]][i-1];
        }
        int len=tree[root].size();
        for(int i=0;i<len;i++)
        {
            int to=tree[root][i];
            if(to==father[root])
            {
                continue;
            }
            father[to]=root;
            deep[to]=deep[root]+1;
            dfs(to);        
        }
        return ; 
    }
    int LCA(int a,int b)
    {
        if(deep[a]<deep[b])
        {
            swap(a,b);
        }
        for(int i=19;i>=0;i--)//将两个点调整到同一高度
        {
            if(deep[b]<=deep[anc[a][i]])
            {
                a=anc[a][i];
            }
        }
        if(a==b)
        {
            return a;
        }
        for(int i=19;i>=0;i--)
        {
            if(anc[a][i]!=anc[b][i])//向上倍增寻找公共祖先
            {
                a=anc[a][i];
                b=anc[b][i];
            }
        }
        return anc[a][0];
    }
    int main()
    {
        int n,m,t;
        scanf("%d %d %d",&n,&m,&t);
        register int a,b;
        for(int i=1;i<=m;i++)
        {
            scanf("%d %d",&a,&b);
            tree[a].push_back(b);
            tree[b].push_back(a);
        }
        father[1]=1;
        dfs(1);
        for(int i=1;i<=t;i++)
        {
            scanf("%d %d",&a,&b);
            cout<<a<<" "<<b<<":";
            cout<<LCA(a,b)<<endl;
        }
        return 0;
    }
    
    

    2.Tarjan算法

      tarjan算法是一个利用dfs遍历和回溯的一个非常巧妙的方法,能在一次遍历内求出任意两点间的LCA的离线算法。


    2.1算法原理

      容易知道,当查询的两个节点u,v在同一棵子树内的时候,距离该子树的根节点最近的点就是LCA。而当两个节点不在同一子树内的时候,则这两个子树所在的子树的根节点就是LCA。

      而这些信息我们都可以在dfs的搜索和回溯通过维护一个集合信息得到。具体步骤如下:

        1.设定u为已访问。

        2.设定u的祖先为u自身。

        3.遍历u的所有邻接点v。

        4.若未访问过,则dfs(v),合并u所在集合和v所在集合为一个新集合,设定新集合的祖先为u若访问过则不再访问。

        5.检查跟这个u点有关的查询(u,v),若v已访问,则lca= v所在集合的祖先,若v未访问不做处理。

      如果上面没理解的话可以看一下下面的代码。


    2.2算法实现

    代码如下
    
    #include <iostream>
    #include <cstdio>
    #include <cmath>
    #include <vector>
    #include <cstdlib>
    #include <cstring>
    #include <algorithm>
    using namespace std;
    vector<int> tree[1005];
    vector<int> ask[1005];
    int father[1005];
    int anc[1005];
    bool used[1005];
    void pre(int n)
    {
        for(int i=1;i<=n;i++)
        {
            father[i]=i;
            tree[i].clear();
            ask[i].clear();
        }
        memset(used,0,sizeof(used));
        return ;
    }
    int find(int x)
    {  
        if(x==father[x])
        {  
            return x;
        }  
        return father[x]=find(father[x]);
    }  
    void unions(int x, int y)
    {  
        x=find(x);  
        y=find(y);  
        if(x==y)   
        {   
            return ;
        }  
        father[x]=y;  
        return ;
    }
    void tarjan(int root)
    {
        anc[root]=root;
        used[root]=1;
        int len=tree[root].size();
        for(int i=0;i<len;i++)
        {
            int to=tree[root][i];
            if(!used[to])
            {
                tarjan(to);
                unions(root,to);
                anc[find(to)]=root;
            }
        }
        len=ask[root].size();
        for(int i=0;i<len;i++)
        {
            int point=ask[root][i];
            if(used[point])
            {
                cout<<root<<" "<<point<<":"<<anc[find(point)]<<endl;//得到最近公共祖先
            }
        }
    }
    int main()
    {
        int n,m,t;
        scanf("%d %d %d",&n,&m,&t);
        register int a,b;
        pre(n);
        for(int i=1;i<=m;i++)
        {
            scanf("%d %d",&a,&b);
            tree[a].push_back(b);
    
        }
        for(int i=1;i<=t;i++)
        {
            scanf("%d %d",&a,&b);
            ask[a].push_back(b);
            ask[b].push_back(a);
        }
        tarjan(1);
        return 0;
    }
    
    

    3.RMQ实现

      RMQ(Range Minimum/Maximum Query)问题指的是区间最值问题,通过使用DFS获得树节点的时间戳,便可以利用RMQ算法预处理后做到在线(O(1))的查询LCA。


    3.1算法原理

      首先有一个显而易见的事实,两个节点的深度最深的祖先便是他们的最近公共祖先。而通过利用DFS标记时间戳,我们便可以在一个区间内知道这两个节点的全部祖先的信息。

      而RMQ算法通常是使用ST表(Sparse Table)实现,具体实现是用(f[i][j])来表示([i,i+2^{j-1}])的区间的最值。而通过动态规划我们便可以预处理出(f)数组。首先(f[i][0])的值就是它本身,而(f[i][j])可以分为((i,i+2^{j-1}-1))((i+2^{j-1},i+2^j-1))两段区间使得两段区间长度都为(2^{j-1})。这样便可以得到状态转移方程(f[i][j]=max(f[i][j-1],f[i+2^{j-1}][j-1]))

      而RMQ的查询可以通过一个中间值(k=log_{2}(j-i+1)),则区间((i,j))的最值就为(max(f[i][k],f[j-2^k+1][k]))。这样便可以在(O(1))的时间内查询区间的最值了。


    3.2算法实现

    代码如下
    
    #include <iostream>
    #include <cstdio>
    #include <vector>
    #include <cstdlib>
    #include <cstring>
    #include <cmath>
    #include <algorithm>
    using namespace std;
    vector<int> tree[1005];
    int cnt=1;
    int st[1005<<1][20];
    int deep[1005<<1];
    int id[1005];
    int idx[1005<<1];
    void getST(int n)//预处理出ST表
    {
        for(int i=1;i<=n;i++)  
        {
            st[i][0]=i;
        }  
        for(int j=1;(1<<j)<=n;j++)  
        {  
            for(int i=1;i+(1<<j)-1<=n;i++)  
            {  
                int a=st[i][j-1];  
                int b=st[i+(1<<(j-1))][j-1];  
                if(deep[a]<=deep[b])  
                {
                    st[i][j]=a;
                }  
                else  
                {
                    st[i][j]=b;
                }
            }
        }
    }
    void dfs(int u,int father,int d)//预处理出时间戳
    {  
        id[u]=cnt;  
        idx[cnt]=u;  
        deep[cnt++]=d;
        int len=tree[u].size();  
        for(int i=0;i<len;i++)  
        {  
            int v=tree[u][i];  
            if(v==father)
            {
                continue;
            }  
            dfs(v,u,d+1);  
            idx[cnt]=u;
            deep[cnt++]=d;  
        }  
        return ;
    }  
    int ask(int l,int r)
    {
        int mid=0;
        while((1<<(mid+1))<=r-l+1)
        {
            mid++;
        }
        int a=st[l][mid];  
        int b=st[r-(1<<mid)+1][mid];  
        if(deep[a]<=deep[b])  
        {    
            return a;
        }  
        else  
        {    
            return b;
        }  
    }
    int LCA(int a,int b)
    {
        int l=id[a];
        int r=id[b];
        if(l>r)
        {
            swap(l,r);
        }
        return idx[ask(l,r)];
    }
    int main()
    {  
        int n,m,t;
        scanf("%d %d %d",&n,&m,&t);
        register int a,b;
        for(int i=1;i<=m;i++)
        {
            scanf("%d %d",&a,&b);
            tree[a].push_back(b);
            tree[b].push_back(a);
        }
        dfs(1,-1,0);
        getST(2*n);
        for(int i=1;i<=t;i++)
        {
            scanf("%d %d",&a,&b);
            cout<<LCA(a,b)<<endl;
        }
        return 0;
    }
    
    

    4.总结

      上面的三种算法中,第一种和第三种属于在线算法,对大多数类型的题目都有着较好的适应性。但倍增法的查询效率要比RMQ的低,但RMQ虽然好理解,但其实现复杂度比较高,如果是考场上还是用倍增来的稳妥。

      而tarjan算法是三种算法中效率最高的,但因其是离线算法所以应用范围不是很广,在面对大量的询问是还是一个不错的算法,且其实现简单,不容易出错。

      在考场上的推荐度为RMQ(leq)Tarjan(leq)倍增。


  • 相关阅读:
    UILabel标签文字过长时的显示方式
    iOS8新特性之交互式通知
    iOS 音频学习
    UISegmentedControl小常识和图片拉伸
    iOS 锁屏判断
    UIwindow的学习
    Mac显示和隐藏系统的隐藏文件
    获取iOS系统版本和设备的电量
    React Native 学习-01
    如何用fir.im 命令行工具 打包上传
  • 原文地址:https://www.cnblogs.com/Heizesi/p/7704847.html
Copyright © 2020-2023  润新知