算法笔记——最近公共祖先(LCA)

利用LCA求解树上任意两点距离

u和v的之间的距离为u到树根上的距离加上v到树根上的距离减去2×lca到树根的距离

dist[u]+dist[v]-2\times dist[lca]

一、暴力搜索法

 

时间复杂度均为O(n)

二、树上倍增法

在树上构造ST表

F[i][j]表示i节点向上走2^{j}步到达的节点。

其中F[i][0]i父节点

递推公式 F[i][j]=F[F[i][j-1],j-1] ,i=1,2,……n,  j=0,1,2,……k,  k=log_{2}n

即走2^{j}步分为两步2^{j-1}

采用同步前进法的思想,但利用倍增的走法。

1、使y到与x同深度的节点

2、x,y同时向上跳

k从大到小遍历,若f[x][i]和f[y][i]相同,说明跳的过高,高于lca,不进行任何操作;

如果不相同,则将x,y上跳至该位置。直到k=0结束后,当前x,y所指节点的父节点即是其lca

时间复杂度:构造ST表需要O(nlogn),单词查询需要O(logn)

模板代码

void dfs(int x)
{
    d[x]=d[f[x][0]]+1;//深度从0开始存
    for(int i=1;(1<<i)<=d[x];i++)
        f[x][i]=f[f[x][i-1]][i-1];
    for(int i=head[x];i;i=nxt[i])
    {
        int y=to[i];
        if(y=f[x][0])    continue;
        f[y][0]=x;
        dfs(y);
    }

}

int lca(int x,int y)
{
    if(d[x]>d[y]) swap(x,y);
    while(d[x]<d[y])
    {
        int k=lg[d[y]-d[x]];
        y=f[y][k];
    }//y上跳至与x同深度
    if(x==y) return x;
    int k=lg[d[x]];
    for(i=k;~i;i--)
    {
        if(f[x][i]!=f[y][i])
            x=f[x][i],y=f[y][i];
    }
    return f[x][0];
}

三、在线RMQ算法

欧拉序列是指在深度遍历过程中把依次经过的节点记录下来,把回溯时经过的节点也记录下来,一个节点可能被记录多次,相当于从树根开始,一笔画出一个经过所有节点的回路

两个节点的LCA一定是两个节点之间的欧拉序列中深度最小的节点。

获得一棵树的欧拉序列,以u,v首次出现的下标为区间端点,利用RMQ查询区间中节点深度的最小值,该节点即为u,v的LCA

void dfs(int x,int d)
{
    vis[x]=true;
    pos[x]=++tot;//首次出现位置,向下搜索时记录
    seq[tot]=x;//欧拉序列
    dep[x]=d;
    for(int i=head[x];i;i=nxt[i])
    {
        int y=to[i];
        if(vis[y]) continue;
        dis[y]=dis[x]+edge[i];
        dfs(y,d+1);
        seq[++tot]=x;//回溯时再次标记
        dep[tot]=d;
    }
}

注意点:F里存储的为欧拉序列的下标,不是深度

void ST_create()
{
    for(int i=1;i<=cnt;i++) f[i][0]=i;//f里存欧拉序列下标,不存深度
    int k=log2(cnt);
    for(int j=1;j<=k;j++)
        for(int i=1;i<=cnt-(1<<j)+1;i++)
            if(dep[f[i][j-1]]<dep[f[i+(1<<(j-1))][j-1]])
                f[i][j]=f[i][j-1];
            else f[i][j]=f[i+(1<<(j-1))][j-1];
}

int RMQ(int l,int r)
{
    int k=log2(r-l+1);
    if(dep[f[l][k]]<dep[f[r-(1<<k)+1][k]])
        return f[l][k];
    else return f[r-(1<<k)+1][k];
}

int lca(int x,int y)
{
    int l=pos[x],r=pos[y];
    if(l>r) swap(l,r);
    return seq[RMQ(l,r)];
}

时间复杂度:

构造ST表需要O(nlogn),单次查询需要O(1)

四、离线Tarjan算法

利用并查集的特性来找lca

操作:

1、初始化集合号数组和访问数组,fa[i]=i,vis[i]=0;

2、从根节点出发深度优先遍历,回溯时记录父子节点关系。

3、当某个节点的邻接点全部遍历完毕时,此时检查关于u的所有查询,若存在查询(u,v),且vis[v]=1,则利用并查集查找v祖宗,该祖宗即为lca

原理:u的祖宗为u向上查找的第一个邻接点没有访问完的节点,而并查集的父子节点关系是在访问完所有邻接节点后更新的,因此这个未更新完的节点当前就是这个集合的祖宗,因而可以通过并查集从v查询到lca

int find(int x){return fa[x]==x?x:fa[x]=find(fa[x]);}

void dfs(int x,int root)
{
    vis[x]=root;
    for(int i=head[x];i;i=nxt[i])
    {
        int y=to[i];
        if(vis[y])  continue;
        dis[y]=dis[x]+edge[i];
        dfs(y,root);
        fa[y]=x;
    }
    for(int i=0;i<q[x].size();i++)
    {
        int y=q[x][i];
        int id=qid[x][i];
        if(vis[y]==vis[x])
        {
            int lca=find(y);
            ans[id]=dis[x]+dis[y]-2*dis[lca];
        }
        else if(vis[y]!=0)  ans[id]=-1;
    }
}

时间复杂度:O(n+m)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值