算法详解(LCA&RMQ&tarjan)补坑啦!完结撒花(。◕ˇ∀ˇ◕)

首先,众所周知,求LCA共有3种算法(树剖就不说了,太高级,以后再学。。)。

1、树上倍增(ST表优化)

2、RMQ&时间戳(ST表优化)

3、tarjan(离线算法)不讲。。(后面补坑啦!)

一、树上倍增

这种方法原理是这样的:

我们可以知道,最朴素的算法就是一步一步的并查集往上找,知道找到2个点并查集相同,即为LCA

但这种算法的时间效率为O(NM)看到0<n,m<5*10^5我们就知道一定会炸。

但是,我们可以发现给出树后,每个点的LCA及走到LCA的路径一定是固定的。

所以可以ST表优化。

首先先BFS出每个点在树上的深度。。(记为depth[i])

接着我们要先让2个点的深度相同,之后2个点一起走,可以加快效率。

最后我们直接倍增上去。(if(fa[i][u]!=fa[i][v])u=fa[i][u];v=fa[i][v];)

最后只要往上走一步,就是LCA了。

重点!倍增时倍增循环在外!不能在内!否则会挂!

下面贴代码(debug了三小时,才调好。膜拜zxyer)

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
struct data{
    int next,to;
}g[500011];
int depth[500011];
int que[1000001];
bool visit[500011];
int head[500011];
int fa[30][500011];
int n,q,root,num=0;
int lca(int u,int v){
    if(depth[u]<depth[v])swap(u,v);
    int dc=depth[u]-depth[v];
    for(int i=0;i<=29;i++)
    if((1<<i)&dc&&fa[i][u]){
    u=fa[i][u];    
    }
    if(u==v)return u;
    for(int i=29;i>=0;i--)
    if(fa[i][u]!=fa[i][v]&&fa[i][u]){
    u=fa[i][u];
    v=fa[i][v];    
    }
    return fa[0][u];
}
void bfs(int u)
{
    memset(que,0,sizeof(que));
    memset(visit,0,sizeof(visit));
    visit[u]=true;
    que[1]=u;
    int h=1,l=1;
    while(h<=l)
    {
        int rt=que[h];
        for (int i=head[rt];i;i=g[i].next)
        {
            int v=g[i].to;
            if ( !visit[v] )
             {
                    visit[v]=true;
                    depth[v]=depth[rt]+1;
                    que[++l]=v;
             }
        }
        h++;   
    }
}
int main(){
    scanf("%d%d",&n,&q);
    memset(g,0,sizeof(g));
    memset(fa,0,sizeof(fa));
    memset(depth,0,sizeof(depth));
    for(int i=1;i<n;i++)
    {
    int f,t;
    scanf("%d%d",&f,&t);
    g[++num].next=head[f];
    head[f]=num;
    g[num].to=t;
    fa[0][t]=f;
    if(fa[0][f]==0)root=f;
    }
    for(int j=1;j<=29;j++)
    for(int i=1;i<=n;i++)
    {
        
        fa[j][i]=fa[j-1][fa[j-1][i]];
    }
    depth[root]=1;
    bfs(root);
    for(int i=1;i<=q;i++){
    {
        int num1,num2;
        scanf("%d%d",&num1,&num2);
        printf("%d
",lca(num1,num2));
    }    
    }
}

二、RMQ+时间戳

这个算法理解了很久才懂

首先我们都知道如果把树看成一个无向图,那么LCA一定在u->v的最短路上。而且,LCA的点就是最短路上depth最小的点。

换句话说,就是LCA是2个点到根节点的路径的第一个交汇处。

接下来用dfs为点标号,用id[i]表示这个点第一次出现在顶点序列的标号。

接下来就是求id[u]<i<id[v]中depth的最小值啦!

这个过程可以用RMQ高速解决。

所以LCA=RMQ(depth)(u,v)

注意!这个算法比较难懂,可以先看看RMQ,理解之后再画画图,恩,就差不多了。

下面贴代码

 #include <cstdio>

#include <cstring>  
#include <queue>  
#include <algorithm>  
#define MAXN 1010  
#define MAXM 100000  
using namespace std;  
struct Edge  
{  
    int from, to, next;  
};  
Edge edge[MAXM];  
int head[MAXN], edgenum;  
int vs[MAXN<<1];//第i次DFS访问节点的编号  
int depth[MAXN<<1];//第i次DFS访问节点的深度  
int id[MAXN];//id[i] 记录在vs数组里面 i节点第一次出现的下标  
int dfs_clock;//时间戳  
int N, M, Q;//点数 边数 查询数  
int dp[MAXN<<1][20];//dp[i][j]存储depth数组  以下标i开始的,长度为2^j的区间里 最小值所对应的下标  
void init()  
{  
    edgenum = 0;  
    memset(head, -1, sizeof(head));  
}  
void addEdge(int u, int v)  
{  
    Edge E = {u, v, head[u]};  
    edge[edgenum] = E;  
    head[u] = edgenum++;  
}  
void getMap()  
{  
    int a, b;  
    while(M--)  
        scanf("%d%d", &a, &b),  
        addEdge(a, b), addEdge(b, a);  
}  
void DFS(int u, int fa, int d)//当前遍历点以及它的父节点  遍历点深度  
{  
    id[u] = dfs_clock;  
    vs[dfs_clock] = u;  
    depth[dfs_clock++] = d;  
    for(int i = head[u]; i != -1; i = edge[i].next)  
    {  
        int v = edge[i].to;  
        if(v == fa) continue;  
        DFS(v, u, d+1);  
        vs[dfs_clock] = u;//类似 回溯  
        depth[dfs_clock++] = d;  
    }  
}  
void find_depth()  
{  
    dfs_clock = 1;  
    memset(vs, 0, sizeof(vs));  
    memset(id, 0, sizeof(id));  
    memset(depth, 0, sizeof(depth));  
    DFS(1, -1, 0);//遍历  
}  
void RMQ_init(int NN)//预处理 区间最小值  
{  
    for(int i = 1; i <= NN; i++)  
        dp[i][0] = i;  
    for(int j = 1; (1<<j) <= NN; j++)  
    {  
        for(int i = 1; i + (1<<j) - 1 <= NN; i++)  
        {  
            int a = dp[i][j-1];  
            int b = dp[i + (1<<(j-1))][j-1];  
            if(depth[a] <= depth[b])  
                dp[i][j] = a;  
            else  
                dp[i][j] = b;  
        }  
    }  
}  
int query(int L, int R)  
{  
    //查询L <= i <= R 里面使得depth[i]最小的值 返回对应下标  
    int k = 0;  
    while((1<<(k+1)) <= R-L+1) k++;  
    int a = dp[L][k];  
    int b = dp[R - (1<<k) + 1][k];  
    if(depth[a] <= depth[b])  
        return a;  
    else  
        return b;  
}  
int LCA(int u, int v)  
{  
    int x = id[u];//比较大小 小的当作左区间 大的当作右区间  
    int y = id[v];  
    if(x > y)  
        return vs[query(y, x)];  
    else  
        return vs[query(x, y)];  
}  
void solve()  
{  
    int a, b;  
    while(Q--)  
    {  
        scanf("%d%d", &a, &b);  
        printf("LCA(%d %d) = %d
", a, b, LCA(a, b));  
    }  
}  
int main()  
{  
    while(scanf("%d%d%d", &N, &M, &Q) != EOF)  
    {  
        init();  
        getMap();  
        find_depth();//DFS遍历整个树 求出所需要的信息  
        RMQ_init(dfs_clock - 1);  
        solve();  
    }  
    return 0;  
}  

重点同上哈!倍增写外面!

 三、tarjan算法求LCA

这个算法和之前的tarjan求强联通分量有一些差别(只是名字一样而已)

这个算法非常妙啊,其实就是从根节点dfs标记父节点,但是厉害的地方在于,在它标记完根节点后,恰好能够更新答案。

这个算法比较绕,网上有很多的图解,我就不贴了,我们来具体算法理解。

下面是tarjan的主体,h就是链表的头。。这个不讲,这里讲一下q数组,它用于存储询问,g[i].to表示他要查询的第二个节点。

void tarjan(int x){
    for(int i=h[x];i;i=g[i].next)
    tarjan(g[i].to),f[g[i].to]=x;
    for(int i=q[x];i;i=g[i].next)
    ans[g[i].to]=ans[g[i].to]>0?getfa(ans[g[i].to]):x;     
}

很显然,在回溯后,我们计算答案,如果没有出现g[i].to的答案,我们就将他标记为x,这是什么意思呢?其实如果标记为X,就说明在dfs时先搜到了x,再搜g[i].to,显然x即为这2者的公共祖先。

但是如果我们已经更新过ans[g[i].to],其实这说明了两个点不在同一子树上,所以我们要找到后一个节点的父亲。(重点来了!)

我们在做getfather的操作的时候,由于是先序遍历,所以我们做到这个dfs时,寻找父亲只会找到他们的公共祖先然后停止。是不是很妙?

嗯对,所以是不是很简单?

#include<iostream>
#include<cstdio>
using namespace std;
int h[500005],q[500005];
bool fa[500005];
int f[500005];
int ans[500005];
int n,m,num=0;
struct edge{
    int to,next;
}g[1500005];
int getfa(int x){return f[x]?f[x]=getfa(f[x]):x;}
void ins(int *h,int u,int v){g[++num].next=h[u];h[u]=num;g[num].to=v;}
void tarjan(int x){
    for(int i=h[x];i;i=g[i].next)
    tarjan(g[i].to),f[g[i].to]=x;
    for(int i=q[x];i;i=g[i].next)
    ans[g[i].to]=ans[g[i].to]>0?getfa(ans[g[i].to]):x;     
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<n;i++){
        int x,y;
        scanf("%d%d",&x,&y);
        ins(h,x,y);    
        fa[y]=1;
    }
    for(int i=0;i<m;i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        ins(q,x,i);ins(q,y,i);
    }
    for(int i=1;i<=n;i++)if(!fa[i]){tarjan(i);break;}
    for(int i=0;i<m;i++)printf("%d
",ans[i]);
    return 0;
}
原文地址:https://www.cnblogs.com/ghostfly233/p/6835850.html