题解 P3379 【【模板】最近公共祖先(LCA)】

蒟蒻学了lca 写篇题解加强理解也方便大家 也建议大家都可以这样试试 理解会更深刻

我自己的代码风格是主函数非常清晰简短主要靠自写函数

lca的做法主要来说有三种:

1.倍增
2.lca转rmq
3.Targan

这些的原理dalao们已经讲得足够清楚了 蒟蒻也不能说的更清晰了 主要说一下三种的特点与代码的细节

(PS:我是真的写的很认真 求通过呀)

特别提醒 请从1.0看起 会讲解代码风格


1.0 倍增

用时: 1399ms / 内存: 53552KB

看到这么大的用时与内存是不是很可怕

但是实际上来讲用了读入与输出优化后过这题是妥妥的

●特点

倍增的特点就是用2的n次方向上爬 爬到同一层后 一起向上爬 虽然比一个一个爬快了许多 但是还是不够优秀

在线处理

●函数讲解

○读入与输出优化

注意的是因为我们是有信仰的oier 所以相信位运算更快

/**读入优化**/
inline void read(int &x){
    x=0;int f=1;char c=getchar();
    while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
    while(c<='9'&&c>='0'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
    x*=f;
    return;
}
/**输出优化**/
inline void print(int x){
    if(x<0) x=-x,putchar('-');
    if(x>9) print(x/10);
    putchar(x%10+48);
    return;
}
○邻接表储存
/**邻接表储存 tot记总数 head标准用法**/
int tot,head[maxn];
struct node{
    int next,v;
}e[maxn*2];
void add(int x,int y)
{
    tot++;
    e[tot].v=y;
    e[tot].next=head[x];
    head[x]=tot;
}
○dfs

主要目的在初始化 f数组便于跳

/**dfs部分 dep记深度 f为[n节点][跳二的次方步]的父亲**/
int dep[maxn],f[maxn][20];
inline void dfs(int x,int fa)
{
    dep[x]=dep[fa]+1;
    f[x][0]=fa;
    for(int i=1;(1<<i)<=dep[x];i++)
    {
        f[x][i]=f[f[x][i-1]][i-1];
    }
    for(int i=head[x];i;i=e[i].next)
    {
        if(e[i].v==fa) continue;
        dfs(e[i].v,x);
    }
}
○lca

上一步已经帮我埋好了伏笔 我再不跳是不是对不起上个函数呀

/**lca 将深度大的跳到与深度小的同一层深度处**/
inline int lca(int x,int y)
{
    if(dep[x]<dep[y]) swap(x,y);
    while(dep[x]>dep[y]) x=f[x][lg[dep[x]-dep[y]]];
    /**这个while非常骚气 保证跳到同一层同时不会乱辈分**/
    if(x==y) return y;
    /**已经是自己了下面就没有意义了**/
    for(int i=lg[dep[x]];i>=0;i--)
        if(f[x][i]!=f[y][i])
            x=f[x][i],y=f[y][i];
    return f[x][0];
}
○主函数
int main()
{
    read(n); read(m); read(rt);
    /**有信仰的读优**/
    for(int i=1;i<n;i++)
        read(a),read(b),add(a,b),add(b,a);
    /**读入部分建立双向边**/
    for(int i=2;i<=n+1;i++)
        lg[i]=lg[i>>1]+1;
    /**预处理以2为底的log函数 (速度小于调用 log2() )**/
    dfs(rt,0);
    /**rt为限定了的根 0默认为根的父亲 其深度为0**/
    for(int i=1;i<=m;i++)
        read(a),read(b),print(lca(a,b)),puts("");
    /**lca 进行查找**/
    return 0;
    /**华丽结束**/
} 

●ps

对于该做法来说 坑点和需要特别注意的点很少 基本上不会出错 除了费时间 没有什么不够优秀的
唯一需要小心的就是双向边存储 内存开两倍

●完整代码

知道你们一直在等这个

/**lca**倍增做法**/
#include<bits/stdc++.h>
using namespace std;
const int maxn=500005;
int lg[maxn],n,m,a,b,rt;
inline void read(int &x){
    x=0;int f=1;char c=getchar();
    while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
    while(c<='9'&&c>='0'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
    x*=f;
    return;
}
inline void print(int x){
    if(x<0) x=-x,putchar('-');
    if(x>9) print(x/10);
    putchar(x%10+48);
    return;
}
int tot,head[maxn];
struct node{
    int next,v;
}e[maxn*2];
void add(int x,int y)
{
    tot++;
    e[tot].v=y;
    e[tot].next=head[x];
    head[x]=tot;
}
int dep[maxn],f[maxn][20];
inline void dfs(int x,int fa)
{
    dep[x]=dep[fa]+1;
    f[x][0]=fa;
    for(int i=1;(1<<i)<=dep[x];i++)
    {
        f[x][i]=f[f[x][i-1]][i-1];
    }
    for(int i=head[x];i;i=e[i].next)
    {
        if(e[i].v==fa) continue;
        dfs(e[i].v,x);
    }
}
inline int lca(int x,int y)
{
    if(dep[x]<dep[y]) swap(x,y);
    while(dep[x]>dep[y]) x=f[x][lg[dep[x]-dep[y]]];
    if(x==y) return y;
    for(int i=lg[dep[x]];i>=0;i--)
        if(f[x][i]!=f[y][i])
            x=f[x][i],y=f[y][i];
    return f[x][0];
}
int main()
{
    read(n); read(m); read(rt);
    for(int i=1;i<n;i++)
        read(a),read(b),add(a,b),add(b,a);
    for(int i=2;i<=n+1;i++)
        lg[i]=lg[i>>1]+1;
    dfs(rt,0);
    for(int i=1;i<=m;i++)
        read(a),read(b),print(lca(a,b)),puts("");
    return 0;
} 

2.0 st表(lca转rmq)

用时: 1528ms / 内存: 99516KB

害怕 居然比倍增慢 那我还学个p呀

我们要重在学习方法与思路

●特点

st表需要找到这棵树的dfs序 即dfn数组 然后记录第一次出现的位置 之后在位置之间寻找深度最小的点

注意f数组需要存的是点的id

●函数讲解

○读入输出优化&&邻接表跳过(同1.0)
○dfs(求欧拉序)

在其他大佬们的证明下,dfn的原因想必大家已经知道了 但是注意的是dfn存的是点的编号

inline void dfs(int x,int fa)
{
    dfn[++cnt]=x;
    dep[x]=dep[fa]+1;
    pos[x]=cnt;
    for(int i=head[x];i;i=e[i].next)
    {
        if(e[i].v==fa) continue;
        dfs(e[i].v,x);
        dfn[++cnt]=x;
    }
}
○对rmq进行预处理

注意:此处的比较带入dep数组 但是存的依然是点的编号!

inline void rmq()
{
    /**f为从后下标开始向后推2的前下标次方个值深度最小的值的点**/
    for(int i=1;i<=cnt;i++)
        f[0][i]=dfn[i];
    /**2的0次方等于1 及它本身就是该值**/
    for(int i=1;i<=20;i++)
    /**根据本题 次方开到20(超百万)保过**/
        for(int j=1;j+(1<<i)-1<=cnt;j++)
        {
            /**注意 比较用的深度 存储用的名称**/
            if(dep[f[i-1][j]]<dep[f[i-1][j+(1<<(i-1))]])
                f[i][j]=f[i-1][j];
            else
                f[i][j]=f[i-1][j+(1<<(i-1))];
        }
}
○lca提取所需要的值

这是rmq的模板了 不会的可以去做做模板题

inline int lca(int x,int y)
{
    x=pos[x];
    y=pos[y];
    if(x>y) swap(x,y);
    int k=lg[y-x+1];
    if(dep[f[k][x]]>dep[f[k][y-(1<<k)+1]]) return f[k][y-(1<<k)+1];
    else return f[k][x];
}
○主函数

大体上于1.0相同

int main()
{
    read(n); read(m); read(rt);
    for(int i=1;i<n;i++)
        read(a),read(b),add(a,b),add(b,a);
    dfs(rt,0);
    /**根据cnt的数对log进行处理**/
    for(int i=2;i<=cnt;i++)
        lg[i]=lg[i>>1]+1;
    rmq();
    for(int i=1;i<=m;i++)
        read(a),read(b),print(lca(a,b)),puts("");
    return 0;
    /**华丽结束**/
} 

●完整代码

#include<bits/stdc++.h>
using namespace std;
const int maxn=500005;
int lg[maxn<<2],n,m,a,b,rt;
inline void read(int &x){
    x=0;int f=1;char c=getchar();
    while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
    while(c<='9'&&c>='0'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
    x*=f;
    return;
}
inline void print(int x){
    if(x<0) x=-x,putchar('-');
    if(x>9) print(x/10);
    putchar(x%10+48);
    return;
}
int tot,head[maxn];
struct node{
    int next,v;
}e[maxn<<1];○
inline void add(int x,int y)
{
    tot++; 
    e[tot].v=y;
    e[tot].next=head[x];
    head[x]=tot;
}
int cnt;
int dep[maxn],pos[maxn],dfn[maxn<<1],f[20][maxn<<1];
inline void dfs(int x,int fa)
{
    dfn[++cnt]=x;
    dep[x]=dep[fa]+1;
    pos[x]=cnt;
    for(int i=head[x];i;i=e[i].next)
    {
        if(e[i].v==fa) continue;
        dfs(e[i].v,x);
        dfn[++cnt]=x;
    }
}
inline void rmq()
{
    for(int i=1;i<=cnt;i++)
        f[0][i]=dfn[i];
    for(int i=1;i<=20;i++)
        for(int j=1;j+(1<<i)-1<=cnt;j++)
        {
            if(dep[f[i-1][j]]<dep[f[i-1][j+(1<<(i-1))]])
                f[i][j]=f[i-1][j];
            else
                f[i][j]=f[i-1][j+(1<<(i-1))];
        }
}
inline int lca(int x,int y)
{
    x=pos[x];
    y=pos[y];
    if(x>y) swap(x,y);
    int k=lg[y-x+1];
    if(dep[f[k][x]]>dep[f[k][y-(1<<k)+1]]) return f[k][y-(1<<k)+1];
    else return f[k][x];
}
int main()
{
    read(n); read(m); read(rt);
    for(int i=1;i<n;i++)
        read(a),read(b),add(a,b),add(b,a);
    dfs(rt,0);
    for(int i=2;i<=cnt;i++)
        lg[i]=lg[i>>1]+1;
    rmq();
    for(int i=1;i<=m;i++)
        read(a),read(b),print(lca(a,b)),puts("");
    return 0;
} 

3.0 Targan

用时: 708ms / 内存: 30140KB

这里必须要膜拜一下Targan了%%%%%

●特点

Targan做法的精髓在使用并查集进行了处理 外加v数组进行判断 不但节省时间而且空间也是妥妥的呀!

tarjan不同的在于是离线处理 一次搞完所有的 然后用ans数组输出

因此需要注意保持顺序

tarjan建了两个邻接表 一个是询问的 一个是输入的 函数部分具体讲解

●函数讲解

○读优输优省略
○邻接表

如果分不清哪个是哪个就好好取名字 不然分不清的时候不要骂博主

/**运用两组邻接表 建立两张图 没有1是二叉树同前 有1是查询**/
int tot,tot1,head[maxn],head1[maxn];
struct node{
    int v,next;
}e[maxn<<1];
struct node1{
    int v,next,num;
}e1[maxn<<1];
/** 双向边 空间*2 **/
inline void add(int x,int y)
{
    tot++;
    e[tot].v=y;
    e[tot].next=head[x];
    head[x]=tot;
}
inline void add1(int i,int x,int y)
{
    /**特别说明:i为输出的关键词 记录的是顺序**/
    tot1++;
    e1[tot1].v=y;
    e1[tot1].next=head1[x];
    e1[tot1].num=i;
    head1[x]=tot1;
}
○并查集
int f[maxn];
inline void unionn(int x,int y)
{
    /**合并**/
    f[x]=y;
}
inline int find(int x)
{
    /**找父亲**/
    if(f[x]!=x) f[x]=find(f[x]);
    return f[x];
}
○dfs

!!!千万注意!!! 返回的时候v再给1 不然遇到找自己的就凉了!

inline void dfs(int x,int fa)
{
    /**第一次循环标记 v及访问过的点**/
    for(int i=head[x];i;i=e[i].next)
    {
        if(e[i].v==fa) continue;
        dfs(e[i].v,x);
        unionn(e[i].v,x);
    }
    v[x]=1;
    /**查找当前标记点的对象有没有以被标记了的**/
    for(int i=head1[x];i;i=e1[i].next)
    {
        if(v[e1[i].v]==0) continue;
        ans[e1[i].num]=find(e1[i].v);
    }
}
○主函数

注意的是如果遇到多组数据同时出现 f数组不需要清0 会重新复制的

int main()
{
    read(n); read(m); read(rt);
    for(int i=1;i<n;i++)
        f[i]=i,read(a),read(b),add(a,b),add(b,a);
    f[n]=n;
    /**为了减少一次循环 将f(并查集)在读入时进行处理**/
    for(int i=1;i<=m;i++)
        read(a),read(b),add1(i,a,b),add1(i,b,a);
    dfs(rt,0);
    for(int i=1;i<=m;i++)
        print(ans[i]),puts("");
    return 0;
} 

●完整代码

#include<bits/stdc++.h>
using namespace std;
const int maxn=500005;
int n,m,a,b,rt,ans[maxn],v[maxn];
inline void read(int &x){
    x=0;int f=1;char c=getchar();
    while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
    while(c<='9'&&c>='0'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
    x*=f;
    return;
}
inline void print(int x){
    if(x<0) x=-x,putchar('-');
    if(x>9) print(x/10);
    putchar(x%10+48);
    return;
}
int tot,tot1,head[maxn],head1[maxn];
struct node{
    int v,next;
}e[maxn<<1];
struct node1{
    int v,next,num;
}e1[maxn<<1];
inline void add(int x,int y)
{
    tot++;
    e[tot].v=y;
    e[tot].next=head[x];
    head[x]=tot;
}
inline void add1(int i,int x,int y)
{
    tot1++;
    e1[tot1].v=y;
    e1[tot1].next=head1[x];
    e1[tot1].num=i;
    head1[x]=tot1;
}
int f[maxn];
inline void unionn(int x,int y)
{
    f[x]=y;
}
inline int find(int x)
{
    if(f[x]!=x) f[x]=find(f[x]);
    return f[x];
}
inline void dfs(int x,int fa)
{
    for(int i=head[x];i;i=e[i].next)
    {
        if(e[i].v==fa) continue;
        dfs(e[i].v,x);
        unionn(e[i].v,x);
    }
    v[x]=1;
    for(int i=head1[x];i;i=e1[i].next)
    {
        if(v[e1[i].v]==0) continue;
        ans[e1[i].num]=find(e1[i].v);
    }
}
int main()
{
    read(n); read(m); read(rt);
    for(int i=1;i<n;i++)
        f[i]=i,read(a),read(b),add(a,b),add(b,a);
    f[n]=n;
    for(int i=1;i<=m;i++)
        read(a),read(b),add1(i,a,b),add1(i,b,a);
    dfs(rt,0);
    for(int i=1;i<=m;i++)
        print(ans[i]),puts("");
    return 0;
} 

好了以上就是全部 总而言之tarjan特别爽 建议理解后背下来 bye!

另外代码都是对的大家不用测试 也希望大家理解后写下来 不要ctrl+C+v

全部评论

相关推荐

不愿透露姓名的神秘牛友
11-27 10:46
点赞 评论 收藏
分享
牛客162194370号:
点赞 评论 收藏
分享
11-08 13:58
门头沟学院 Java
程序员小白条:竟然是蓝桥杯人才doge,还要花钱申领的offer,这么好的公司哪里去找
点赞 评论 收藏
分享
评论
点赞
收藏
分享
牛客网
牛客企业服务