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\)倍增。


posted @ 2017-10-21 15:12  黑泽斯  阅读(297)  评论(2编辑  收藏  举报