首页 > 笔记 > LCA问题详解

LCA问题详解

好久以前写的,也没用latex啥的,一直忘了放出来。(好像是noip前

离线算法 Tarjan

利用并查集优越的时空复杂度,我们可以实现LCA问题的O(n+Q)算法,这里Q表示询问的次数。

Tarjan算法基于深度优先搜索的框架,对于新搜索到的一个结点,首先创建由这个结点构成的集合,再对当前结点的每一个子树进行搜索,每搜索完一棵子树,则可确定子树内的LCA询问都已解决。其他的LCA询问的结果必然在这个子树之外,这时把子树所形成的集合与当前结点的集合合并,并将当前结点设为这个集合的祖先。之后继续搜索下一棵子树,直到当前结点的所有子树搜索完。这时把当前结点也设为已被检查过的,同时可以处理有关当前结点的LCA询问,如果有一个从当前结点到结点v的询问,且v已被检查过,则由于进行的是深度优先搜索,当前结点与v的最近公共祖先一定还没有被检查,而这个最近公共祖先的包涵v的子树一定已经搜索过了,那么这个最近公共祖先一定是v 所在集合的祖先。

这种算法在询问少的时候比较优越。但是我觉得不大实用就没写。

在线ST+dfs

RMQ(Range Minimum/Maximum Query)问题是求区间最值问题。可以写一个线段树,但是预处理和查询的复杂度都是O(logn)。这里有更牛的算法,就是ST(Sparse Table)算法,它可以做到O(nlogn)的预处理,O(1)地回答每个询问。

来看一下ST算法是怎么实现的(以最大值为例):
首先是预处理,用一个DP解决。设a[i]是要求区间最值的数列,f[i,j]表示从第i个数起连续2j个数中的最大值。例如数列

3 2 4 5 6 8 1 2 9 7

f[1,0]表示第1个数起,长度为20=1个数中的最大值,其实只有3这个数。f[1,2]=5,f[1,3]=8,f[2,0]=2,f[2,1]=4……从这里可以看出f[i,0]其实就等于a[i]。这样,Dp的状态、初值都已经有了,剩下的就是状态转移方程。我们把f[i,j]的求值区间平均分成两段(因为2j一定是偶数),从i到i+2j-1-1为一段,i+2j-1到i+2j-1为一段(长度都为2j-1)。用上例说明,当i=1,j=3时就是3,2,4,5 和 6,8,1,2这两段。f[i,j]就是这两段的最大值中的最大值。于是我们得到了动规方程

f[i][j]=max(f[i][j-1],f[i+2j-1][j-1])

F[i,j]表称为稀疏表(Sparse Table)。有了稀疏表,我们就可以做到O(1)时间查询任意区间的最值。如在上例中我们要求区间[2,8]的最大值,就要把它分成[2,5]和[5,8]两个区间,因为这两个区间长度为22,最大值我们可以直接由f[2,2]和f[5,2]得到。扩展到一般情况,就是把区间[l,r]分成两个长度为2j的区间(保证有f[i,j]对应,其中2j为小于r-l+1的最大值)。即,q[l,r] 表示 从第l个数到第r个数中最大的数,那么:

q[l][r]=max(f[l][j],f[r-2j+1][j]);

 编程时,注意1<<i=2^i

例题 luogu1816

题解

#include <cstdio>
#include <iostream>
#include <cstring>
using namespace std;
int f[100010][100];
int a[100010];
int n,q,x,y;
void rmq_init()
{
    for (int i=1;i<=n;i++)
        f[i][0]=a[i];
    int i=1,j=1;
    for (int j=1;(1<<j)<n;j++)
        for (int i=1;i+(1<<j)<=n;i++)
            f[i][j]=min(f[i][j-1],f[i+(1<<(j-1))][j-1]);
}
int rmq_query(int l,int r)
{
    int pos;
    for (pos=0;(1<<pos)<=(r-l+1);pos++);
    pos--;
    return (min(f[l][pos],f[r-(1<<pos)+1][pos]));
}
main()
{
    memset(f,0x7f,sizeof f);
    scanf("%d%d",&n,&q);
    for (int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    rmq_init();
    for (int i=1;i<=q;i++)
    {
        scanf("%d%d",&x,&y);
        printf("%d ",rmq_query(x,y));
    }
}

好了,回归我们的LCA问题

LCA 问题可以转化为 RMQ 问题求解,方法为:

观察:节点u与v的LCA正是对树T进行深度优先遍历过程中u节点与v节点之间的具有最浅深度的节点,下图展示了对一颗树的深度优先遍历过程,以及形成的欧拉迹。

images

注:设树节点数为n,则欧拉迹的大小为2n-1
对于一颗树T,我们创建三个数组: E [1,..,2n-1],存放树T的欧拉迹,E[i]表示欧拉迹中第i个节点的标号;L [1,..2n-1],存放欧拉迹中各节点的深度,根节点深度为0; R [1,..n],R[i]存放第i个节点在欧拉迹中第一次出现的位置。例如:

images
为了计算LCAT(x,y):
欧拉迹中处于x和y之间的所在节点为ER[x],..,R[y]。在这个子迹中,最浅节点的标号为RMQL(R[x],R[y]),这里RMQ算法查找最小节点的标号。此时LCAT(x,y)= RMQL(R[x],R[y])

代码

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#define M 500000*2+10
#define N 500000*2+10
using namespace std;

struct node
{
    int to,next;
}e[M];
int tot,st[M];
int n,m,root,x,y;
int f[N][20];
int dfs_list[N],depth[N],first[N];

void add(int x,int y)
{
    e[++tot].to=y;
    e[tot].next=st[x];
    st[x]=tot;
}

int pow_2(int j)
{
    return (1<<j);
}

void rmq_init()
{
    for (int i=1;i<=2*n-1;i++)
        f[i][0]=i;
    int i=1,j=1;
    for (int j=1;pow_2(j)<=2*n-1;j++)
        for (int i=1;i+pow_2(j)<=2*n-1;i++)
        {
            int a=depth[f[i][j-1]];
            int b=depth[f[i+pow_2((j-1))][j-1]];
            if (a<b)
            f[i][j]=f[i][j-1];
            else
            f[i][j]=f[i+pow_2((j-1))][j-1];
        }
}
int rmq_query(int l,int r)
{
    int pos;
    for (pos=0;pow_2(pos)<(r-l+1);pos++);
    pos--;
    int a=depth[f[l][pos]];
    int b=depth[f[r-pow_2(pos)+1][pos]];
    if (a<b)
        return dfs_list[f[l][pos]];
    else
        return dfs_list[f[r-(1<<pos)+1][pos]];
}

int dfs_clock=0;
void dfs(int now,int pre,int deep)
{
    dfs_list[++dfs_clock]=now;
    depth[dfs_clock]=deep;
    if (!first[now])
        first[now]=dfs_clock;
    for (int i=st[now];i;i=e[i].next)
        if (e[i].to!=pre)
        {
            dfs(e[i].to,now,deep+1);
            dfs_list[++dfs_clock]=now;
            depth[dfs_clock]=deep;
        }
}

main()
{
    scanf("%d%d%d",&n,&m,&root);
    for (int i=1;i<n;i++)
        scanf("%d%d",&x,&y),add(x,y),add(y,x);
    dfs(root,-1,0);
    rmq_init();
    for (int i=1;i<=m;i++)
    {
        scanf("%d%d",&x,&y);
        if (first[x]>first[y]) swap(x,y);
        //printf("%d %d\n",first[x],first[y]);
        printf("%d\n",rmq_query(first[x],first[y]));
    }
}