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 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
vector 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=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<

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 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
vector tree[1005];
vector 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

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 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
vector 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<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<

4.总结

  上面的三种算法中,第一种和第三种属于在线算法,对大多数类型的题目都有着较好的适应性。但倍增法的查询效率要比RMQ的低,但RMQ虽然好理解,但其实现复杂度比较高,如果是考场上还是用倍增来的稳妥。
  而tarjan算法是三种算法中效率最高的,但因其是离线算法所以应用范围不是很广,在面对大量的询问是还是一个不错的算法,且其实现简单,不容易出错。
  在考场上的推荐度为RMQ\(\leq\)Tarjan\(\leq\)倍增。


你可能感兴趣的:(NOIP模板复习(2) LCA的三种解法)