Luogu p3379(LCA)

传送门

题意:

求一颗nn个节点的树的LCALCA

题目分析:

复习+学习一下三种不同LCALCA的求法(特别是根据欧拉序+STST表求LCALCA)的方法。

下面简单总结(借鉴)一下LCALCA的三种求法

代码:

  • 树上倍增算法(在线),预处理时间复杂度O(nlogn)\mathcal{O}(nlogn),每次询问的时间复杂度为O(logn)\mathcal{O}(logn)
    该算法的核心为构建出一个倍增数组ance(i,j)ance(i,j),表示第ii个结点的第2j2^{j}个祖先。同时存在一个递推式:ance(i,j)=ance( ance(i,j1),j1 )ance(i,j)=ance( ~ance(i,j-1),j-1~ )
    因此我们可以先预处理出来所有的ance(i,j)ance(i,j)数组,最后对于每个询问,都根据ance(i,j)ance(i,j)进行跳转,就可以用O(logn)\mathcal{O}(logn)的时间复杂度求出两个结点u,vu,vLCALCA
#include <bits/stdc++.h>
#define maxn 1000005
using namespace std;
const int LOG=20;
struct Node{
    int to,next;
}q[maxn];
int head[maxn],cnt=0;
void add_edge(int from,int to){
    q[cnt].to=to;
    q[cnt].next=head[from];
    head[from]=cnt++;
}
struct LCA_ST{
    int anc[maxn][LOG],depth[maxn];
    void dfs(int x,int fa,int dis){
        anc[x][0]=fa;depth[x]=dis;
        for(int i=head[x];i!=-1;i=q[i].next){
            int to=q[i].to;
            if(to==fa) continue;
            dfs(to,x,dis+1);
        }
    }
    void init(int root,int n){
        dfs(root,-1,1);
        for(int j=1;j<LOG;j++){
            for(int i=1;i<=n;i++){
                anc[i][j]=anc[ anc[i][j-1] ][j-1];
            }
        }
    }
    void swim(int &x,int h){
        for(int i=0;h>0;i++){
            if(h&1)
                x=anc[x][i];
            h>>=1;
        }
    }
    int query(int x,int y){
        if(depth[x]<depth[y]) swap(x,y);
        swim(x,depth[x]-depth[y]);
        if(x==y) return x;
        for(int i=LOG-1;i>=0;i--){
            if(anc[x][i]!=anc[y][i]){
                x=anc[x][i];
                y=anc[y][i];
            }
        }
        return anc[x][0];
    }
}lca;
int main()
{
    memset(head,-1,sizeof(head));
    cnt=0;
    int n,m,root;
    scanf("%d%d%d",&n,&m,&root);
    for(int i=0;i<n-1;i++){
        int a,b;
        scanf("%d%d",&a,&b);
        add_edge(a,b);
        add_edge(b,a);
    }
    lca.init(root,n);
    while(m--){
        int a,b;
        scanf("%d%d",&a,&b);
        printf("%d\n",lca.query(a,b));
    }
    return 0;
}

dfs会爆栈?我们也可以用bfs进行预处理

#include <bits/stdc++.h>
#define maxn 1000005
using namespace std;
const int LOG=20;
struct Node{
    int to,next;
}q[maxn];
int head[maxn],cnt=0;
void add_edge(int from,int to){
    q[cnt].to=to;
    q[cnt].next=head[from];
    head[from]=cnt++;
}
struct LCA_ST{
    int anc[maxn][LOG],depth[maxn];
    void bfs(int root){
        queue<int>que;
        depth[root]=0;
        anc[root][0]=root;
        que.push(root);
        while(!que.empty()){
            int x=que.front();
            que.pop();
            for(int i=1;i<LOG;i++){
                anc[x][i]=anc[ anc[x][i-1] ][i-1];
            }
            for(int i=head[x];i!=-1;i=q[i].next){
                int to=q[i].to;
                if(to==anc[x][0]) continue;
                depth[to]=depth[x]+1;
                anc[to][0]=x;
                que.push(to);
            }
        }
    }
    void swim(int &x,int h){
        for(int i=0;h>0;i++){
            if(h&1)
                x=anc[x][i];
            h>>=1;
        }
    }
    int query(int x,int y){
        if(depth[x]<depth[y]) swap(x,y);
        swim(x,depth[x]-depth[y]);
        if(x==y) return x;
        for(int i=LOG-1;i>=0;i--){
            if(anc[x][i]!=anc[y][i]){
                x=anc[x][i];
                y=anc[y][i];
            }
        }
        return anc[x][0];
    }
}lca;
int main()
{
    memset(head,-1,sizeof(head));
    cnt=0;
    int n,m,root;
    scanf("%d%d%d",&n,&m,&root);
    for(int i=0;i<n-1;i++){
        int a,b;
        scanf("%d%d",&a,&b);
        add_edge(a,b);
        add_edge(b,a);
    }
    lca.bfs(root);
    while(m--){
        int a,b;
        scanf("%d%d",&a,&b);
        printf("%d\n",lca.query(a,b));
    }
    return 0;
}

  • 欧拉序+STST表(在线),预处理时间复杂度为O(n+nlogn)\mathcal{O}(n+nlogn),每次询问的时间复杂度为O(1)\mathcal{O}(1)
    个人认为,该算法是三个求解LCALCA算法中最浅显易懂的算法了。
    首先我们需要知道,欧拉序即为:对有根树TT进行深度优先遍历,无论是递归还是回溯,每次到达一个节点就把编号记录下来,得到一个长度为2N12*N-1的序列,成为树 TT 的欧拉序列 FF
    之后我们再记录一下出现的每个结点nodenode第一次出现的位置first(node)first(node),最后我们可以发现,LCA(u,v)LCA(u,v)等价于在欧拉序列中区间在first(u)first(u)first(v)first(v)之间的深度最小的结点。
    因此,此时我们就可以将求解LCALCA转化成一个RMQRMQ问题,继而,我们就可以用STST表进行维护。
    继而预处理的时间复杂度为O(n+nlogn)\mathcal{O}(n+nlogn),而单次的查询可以达到O(1)\mathcal{O}(1)
#include <bits/stdc++.h>
#define maxn 1000005
using namespace std;
const int LOG=20;
struct Node{
    int to,next;
}q[maxn<<1];
int head[maxn],cnt=0;
void add_edge(int from,int to){
    q[cnt].to=to;
    q[cnt].next=head[from];
    head[from]=cnt++;
}
struct LCA_ST{
    int ST[maxn<<1][LOG];
    int value[maxn],depth[maxn<<1],first[maxn],len;
    int cal(int x,int y){
        return depth[x]<depth[y]?x:y;
    }
    void dfs(int x,int fa,int dis){
        value[++len]=x,depth[len]=dis;
        first[x]=len;
        for(int i=head[x];i!=-1;i=q[i].next){
            int to=q[i].to;
            if(to==fa) continue;
            dfs(to,x,dis+1);
            value[++len]=x;
            depth[len]=dis;
        }
    }
    void init(int root){
        len=0;
        dfs(root,-1,1);
        for(int i=1;i<=len;i++) ST[i][0]=i;
        for(int j=1;(1<<j)<=len;j++){
            for(int i=1;i+(1<<j)-1<=len;i++){
                ST[i][j]=cal(ST[i][j-1],ST[i+(1<<(j-1))][j-1]);
            }
        }
    }
    int query(int x,int y){
        int l=first[x],r=first[y];
        if(l>r) swap(l,r);
        int k=log2(r-l+1);
        return value[cal(ST[l][k],ST[r-(1<<k)+1][k])];
    }
}lca;
int main()
{
    memset(head,-1,sizeof(head));
    cnt=0;
    int n,m,root;
    scanf("%d%d%d",&n,&m,&root);
    for(int i=0;i<n-1;i++){
        int a,b;
        scanf("%d%d",&a,&b);
        add_edge(a,b);
        add_edge(b,a);
    }
    lca.init(root);
    while(m--){
        int a,b;
        scanf("%d%d",&a,&b);
        printf("%d\n",lca.query(a,b));
    }
    return 0;
}
  • TarjanTarjan算法有待总结……
posted @ 2019-03-22 21:41  ChenJr  阅读(111)  评论(0编辑  收藏  举报