LCA(离线Tarjan算法,在线倍增法)详解
首先我们看一道洛谷上的板子题:
P3379 【模板】最近公共祖先(LCA)
时空限制:1000ms,128M
【题目描述】
如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先。
【输入格式】
第一行包含三个正整数N、M、S,分别表示树的结点个数、询问的个数和树根结点的序号。
接下来N-1行每行包含两个正整数x、y,表示x结点和y结点之间有一条直接连接的边(数据保证可以构成树)。
接下来M行每行包含两个正整数a、b,表示询问a结点和b结点的最近公共祖先。
【输出格式】
输出包含M行,每行包含一个正整数,依次为每一个询问的结果。
【输入样例】
5 5 4
3 1
2 4
5 1
1 4
2 4
3 2
3 5
1 2
4 5
【输出样例】
4
4
1
4
4
【数据范围】
对于30%的数据:N<=10,M<=10;
对于70%的数据:N<=10000,M<=10000;
对于100%的数据:N<=500000,M<=500000;
这题读完就有一个想法,就是建树,然后一直往往父节点身上找。
但是时间复杂度是O(N*M)的,也就是O(25*10^10)不要想了,70分都难拿到手。
这时,就要用到LCA了。
下面介绍两种方法:
离线Tarjan算法
操作步骤如图:
假设是这样一棵树。
第一次遍历到底(红色表示被遍历的点,图片有点难看,见谅)。
如果询问6和4,它们的最近公共祖先是4。
第二次遍历到底(蓝色表示遍历完的点)。
当此时询问7和6时,它们的最近公共祖先find(6)(find(x)在后面会解释,因为这条线路上都是7的祖先,所以不用find(7)),也就是4。
第三次遍历到底。
如果求8和7的最近公共祖先,那么就是find(7),就是2。
第四次遍历到底。
如果求3和4的最近公共祖先,那么就是find(4),就是1。
通过以上图片,我们知道find(x)表示离自己最近的被遍历过的点(跳过被遍历完的点)。还有前提,两个点都被遍历过。但是如果这样,还是很慢啊。
其实可以用并查集来写find(x),这样不是快多了?O(1)出答案,但是遍历的过程是O(N)的效率,因为有M次询问,所以总时间效率是O(N+M)。
但是答案是在过程中得出的,是无序的(所以我们需要使它有序,在代码中详解)。
#include<cstdio>
#define MAXN 500005
using namespace std;
int n,q,m,fa[MAXN];
int lnk[2][MAXN],nxt[2][2*MAXN],son[2][2*MAXN],tot[2],ans[MAXN];//ans[]存答案
bool vis[MAXN];
void add(int a,int x,int y){son[a][++tot[a]]=y;nxt[a][tot[a]]=lnk[a][x];lnk[a][x]=tot[a];}//邻接表
int read(){//读入
int ret=0;bool f=1;char ch=getchar();
for(;ch<'0'||'9'<ch;ch=getchar()) f^=!(ch^'-');
for(;ch>='0'&&ch<='9';ch=getchar()) ret=(ret<<3)+(ret<<1)+ch-48;
return f?ret:-ret;
}
int get(int x){return fa[x]==x?x:fa[x]=get(fa[x]);}//并查集
int mer(int x,int y){//合并
int fx=get(x),fy=get(y);
if(fx!=fy) fa[fy]=fx;
}
int DFS(int x){//LCA用DFS实现
vis[x]=1;
for(int j=lnk[1][x];j;j=nxt[1][j])//枚举问题
if(vis[son[1][j]]) ans[(j+1)/2]=get(son[1][j]);//因为是邻接表,所以j就是读入的序号,但是因为存入了询问(a,b)(b,a)两个,所以要(j+1)/2(奇偶性)
for(int j=lnk[0][x];j;j=nxt[0][j])
if(!vis[son[0][j]]) DFS(son[0][j]),mer(x,son[0][j]);//接下去遍历它的儿子节点 ,遍历完后合并。
}
int main(){
freopen("LCA.in","r",stdin);
freopen("LCA.out","w",stdout);
n=read();q=read();m=read();//q是寻问数 ,m是根节点。
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1;i<n;i++){
int x=read(),y=read();
add(0,x,y);add(0,y,x);
}
for(int i=1;i<=q;i++){
int a=read(),b=read();
add(1,a,b);add(1,b,a);//因为有可能出现a被遍历到了,而b没有,然后就无法输出答案。
}
DFS(m);
for(int i=1;i<=q;i++) printf("%d\n",ans[i]);
return 0;
}
在线倍增法
这个就有点像RMQ了,其实就是树上RMQ罢了,f[i][j]表示从i开始,向上2j的节点编号,我们设x=f[i][j−1]那么,f[i][j]=f[x][j−1]。
取答案的时候,先将两个数提到同一高的,然后,一同往上跳,就能做到O(n*log(n))的处理,O(log(n))的取答案。
代码如下:
#include<cstdio>
#include<iostream>
#define MAXN 500005
using namespace std;
int n,q,g,fa[MAXN];
int lnk[MAXN],nxt[2*MAXN],son[2*MAXN],tot,f[MAXN][30],dep[MAXN];//dep[i]是i这个节点的深度
bool vis[MAXN];
void add(int x,int y){son[++tot]=y;nxt[tot]=lnk[x];lnk[x]=tot;}//邻接表
int read(){//读入
int ret=0;bool f=1;char ch=getchar();
for(;ch<'0'||'9'<ch;ch=getchar()) f^=!(ch^'-');
for(;ch>='0'&&ch<='9';ch=getchar()) ret=(ret<<3)+(ret<<1)+ch-48;
return f?ret:-ret;
}
void DFS(int x,int deep){//无根树变成有根树
dep[x]=deep;
for(int j=lnk[x];j;j=nxt[j])
if(!vis[son[j]]) vis[son[j]]=1,fa[son[j]]=x,DFS(son[j],deep+1);
}
void change(){
for(int i=1;i<=n;i++){
f[i][0]=fa[i];
for(int j=1;(1<<j)<n;j++) f[i][j]=-1;//赋初值,无祖先为-1
}
for(int j=1;(1<<j)<n;j++)
for(int i=1,x;i<=n;i++) if(f[i][j-1]^-1) x=f[i][j-1],f[i][j]=f[x][j-1];//^可以理解为!=,但是^更快
}
int get(int p,int q){
int log;
if(dep[p]<dep[q]) swap(p,q);//我们设p的深度大
for(log=1;(1<<log)<=dep[p];log++);log--;
for(int i=log;i+1;i--)//将p往上提,"i+1"可以认为是i>=0
if(dep[p]-(1<<i)>=dep[q]) p=f[p][i];
if(p==q) return p;//或q
for(int i=log;i+1;i--)//同时往上跳
if(f[p][i]^-1&&f[p][i]^f[q][i]) p=f[p][i],q=f[q][i];
return fa[p];//或fa[q];
}
int main(){
n=read(),q=read(),g=read();
for(int i=1;i<n;i++){
int x=read(),y=read();
add(x,y);add(y,x);
}
fa[g]=g;vis[g]=1;//原点打掉
DFS(g,0);change();
for(int i=1;i<=n;i++){
int x=read(),y=read();
printf("%d\n",get(x,y));
}
return 0;
}
如果还不懂,可以在评论里提问,我看到会解答的哦