图论--最近公共祖先问题(LCA)模板

时间:2021-11-22 22:07:16
图论--最近公共祖先问题(LCA)模板

最近公共祖先问题(LCA)是求一颗树上的某两点距离他们最近的公共祖先节点,由于树的特性,树上两点之间路径是唯一的,所以对于很多处理关于树的路径问题的时候为了得知树两点的间的路径,LCA是几乎最有效的解法。

首先是LCA的倍增算法。算法主体是依靠首先对整个树的预处理DFS,用来预处理出每个点的直接父节点,同时可以处理出每个点的深度和与根节点的距离,然后利用类似RMQ的思想处理出每个点的 2 的幂次的祖先节点,这就可以用 nlogn 的时间完成整个预处理的工作。然后每一次求两个点的LCA时只要对两个点深度经行考察,将深度深的那个利用倍增先爬到和浅的同一深度,然后一起一步一步爬直到爬到相同节点,就是LCA了。

具体模板是从鹏神的模板小改来的。

注释方便理解版:

 #include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std; const int maxn=1e5+;
const int maxm=1e5+;
const int maxl=; //总点数的log范围,一般会开稍大一点 int fa[maxl][maxn],dep[maxn],dis[maxn]; //fa[i][j]是j点向上(不包括自己)2**i 层的父节点,dep是某个点的深度(根节点深度为0),dis是节点到根节点的距离
int head[maxn],point[maxm],nxt[maxm],val[maxm],size;
int n; void init(){
size=;
memset(head,-,sizeof(head));
} void add(int a,int b,int v){
point[size]=b;
val[size]=v;
nxt[size]=head[a];
head[a]=size++;
point[size]=a;
val[size]=v;
nxt[size]=head[b];
head[b]=size++;
} void Dfs(int s,int pre,int d){ //传入当前节点标号,父亲节点标号,以及当前深度
fa[][s]=pre; //当前节点的上一层父节点是传入的父节点标号
dep[s]=d;
for(int i=head[s];~i;i=nxt[i]){
int j=point[i];
if(j==pre)continue;
dis[j]=dis[s]+val[i];
Dfs(j,s,d+);
}
} void Pre(){
dis[]=;
Dfs(,-,);
for(int k=;k+<maxl;++k){ //类似RMQ的做法,处理出点向上2的幂次的祖先。
for(int v=;v<=n;++v){
if(fa[k][v]<)fa[k+][v]=-;
else fa[k+][v]=fa[k][fa[k][v]]; //处理出两倍距离的祖先
}
}
} int Lca(int u,int v){
if(dep[u]>dep[v])swap(u,v); //定u为靠近根的点
for(int k=maxl-;k>=;--k){
if((dep[v]-dep[u])&(<<k)) //根据层数差值的二进制向上找v的父亲
v=fa[k][v];
}
if(u==v)return u; //u为v的根
for(int k=maxl-;k>=;--k){
if(fa[k][u]!=fa[k][v]){ //保持在相等层数,同时上爬寻找相同父节点
u=fa[k][u];
v=fa[k][v];
}
}
return fa[][u]; //u离lca只差一步
}

木有注释版:

 #include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std; const int maxn=1e5+;
const int maxm=1e5+;
const int maxl=; int fa[maxl][maxn],dep[maxn],dis[maxn];
int head[maxn],point[maxm],nxt[maxm],val[maxm],size;
int n; void init(){
size=;
memset(head,-,sizeof(head));
} void add(int a,int b,int v){
point[size]=b;
val[size]=v;
nxt[size]=head[a];
head[a]=size++;
point[size]=a;
val[size]=v;
nxt[size]=head[b];
head[b]=size++;
} void Dfs(int s,int pre,int d){
fa[][s]=pre;
dep[s]=d;
for(int i=head[s];~i;i=nxt[i]){
int j=point[i];
if(j==pre)continue;
dis[j]=dis[s]+val[i];
Dfs(j,s,d+);
}
} void Pre(){
dis[]=;
Dfs(,-,);
for(int k=;k+<maxl;++k){
for(int v=;v<=n;++v){
if(fa[k][v]<)fa[k+][v]=-;
else fa[k+][v]=fa[k][fa[k][v]];
}
}
} int Lca(int u,int v){
if(dep[u]>dep[v])swap(u,v);
for(int k=maxl-;k>=;--k){
if((dep[v]-dep[u])&(<<k))
v=fa[k][v];
}
if(u==v)return u;
for(int k=maxl-;k>=;--k){
if(fa[k][u]!=fa[k][v]){
u=fa[k][u];
v=fa[k][v];
}
}
return fa[][u];
}

静态树上路径求最小值:LCA倍增

 #include<bits/stdc++.h>
using namespace std; const int maxn=1e6+;
const int maxm=2e6+;
const int maxl=;
const int INF = 0x3f3f3f3f; int fa[maxl][maxn],dep[maxn],dis[maxl][maxn];
int head[maxn],point[maxm],nxt[maxm],val[maxm],size;
int vis[maxn];
int n,q,tmp=INF; void init(){
size=;
memset(head,-,sizeof(head));
memset(vis,,sizeof(vis));
} void add(int a,int b){
point[size]=b;
nxt[size]=head[a];
head[a]=size++;
point[size]=a;
nxt[size]=head[b];
head[b]=size++;
} void Dfs(int s,int pre,int d){
fa[][s]=pre;
dis[][s]=s;
dep[s]=d;
for(int i=head[s];~i;i=nxt[i]){
int j=point[i];
if(j==pre)continue;
Dfs(j,s,d+);
}
} void Pre(){
Dfs(,-,);
for(int k=;k+<maxl;++k){
for(int v=;v<=n;++v){
if(fa[k][v]<)fa[k+][v]=-;
else fa[k+][v]=fa[k][fa[k][v]];
if(fa[k][v]<)dis[k+][v]=dis[k][v];
else dis[k+][v]=min(dis[k][v],dis[k][fa[k][v]]);
}
}
} int Lca(int u,int v){
tmp = min( u, v );
if(dep[u]>dep[v])swap(u,v);
for(int k=maxl-;k>=;--k){
if((dep[v]-dep[u])&(<<k)){
tmp = min( tmp, dis[k][v]);
v=fa[k][v];
}
}
tmp = min( tmp,v );
if(u==v)return u;
for(int k=maxl-;k>=;--k){
if(fa[k][u]!=fa[k][v]){
tmp=min(tmp,min(dis[k][u],dis[k][v]));
u=fa[k][u],v=fa[k][v];
}
}
tmp = min( tmp, min(u,v));
tmp = min( tmp, fa[][u]);
return fa[][u];
}
//tmp即为u、v路径上的最小值

离线Tarjan的做法主要是防止由于每个点对可能被询问多次,导致每次求都需要 logn 的时间,会超时,所以离线来一并处理所有的询问。

Tarjan的做法是通过递归到最底层,然后开始不断递归回去合并并查集,这样就能够在访问完每个点之后赋值它有关切另一个点已经被访问过的询问。

同样是鹏神的模板修改成自己的代码风格后的。

注释版:

 #include<stdio.h>        //差不多要这些头文件
#include<string.h>
#include<vector>
#include<algorithm>
using namespace std; const int maxn=1e5+; //点数、边数、询问数
const int maxm=2e5+;
const int maxq=1e4+; int n;
int head[maxn],nxt[maxm],point[maxm],val[maxm],size;
int vis[maxn],fa[maxn],dep[maxn],dis[maxn];
int ans[maxq];
vector<pair<int,int> >v[maxn]; //记录询问、问题编号 void init(){
memset(head,-,sizeof(head));
size=;
memset(vis,,sizeof(vis));
for(int i=;i<=n;++i){
v[i].clear();
fa[i]=i;
}
dis[]=dep[]=;
} void add(int a,int b,int v){
point[size]=b;
val[size]=v;
nxt[size]=head[a];
head[a]=size++;
point[size]=a;
val[size]=v;
nxt[size]=head[b];
head[b]=size++;
} int find(int x){
return x==fa[x]?x:fa[x]=find(fa[x]);
} void Tarjan(int s,int pre){
for(int i=head[s];~i;i=nxt[i]){
int j=point[i];
if(j!=pre){
dis[j]=dis[s]+val[i];
dep[j]=dep[s]+;
Tarjan(j,s); //这里Tarjan的DPS操作必须在并查集合并之前,这样才能保证求lca的时候lca是每一小部分合并时的祖先节点,如果顺序交换,那么所有的查询都会得到 1 节点,就是错误的
int x=find(j),y=find(s);
if(x!=y)fa[x]=y;
}
}
vis[s]=;
for(int i=;i<v[s].size();++i){
int j=v[s][i].first;
if(vis[j]){
int lca=find(j);
int id=v[s][i].second;
ans[id]=lca; //这里视题目要求给答案赋值
// ans[id]=dep[s]+dep[j]-2*dep[lca];
// ans[id]=dis[s]+dis[j]-2*dis[lca];
}
}
} for(int i=;i<=k;++i){ //主函数中的主要部分
int a,b;
scanf("%d%d",&a,&b);
v[a].push_back(make_pair(b,i)); //加问题的时候两个点都要加一次
v[b].push_back(make_pair(a,i));
}
Tarjan(,);

木有注释版:

 #include<stdio.h>
#include<string.h>
#include<vector>
#include<algorithm>
using namespace std; const int maxn=1e5+;
const int maxm=2e5+;
const int maxq=1e4+; int n;
int head[maxn],nxt[maxm],point[maxm],val[maxm],size;
int vis[maxn],fa[maxn],dep[maxn],dis[maxn];
int ans[maxq];
vector<pair<int,int> >v[maxn]; void init(){
memset(head,-,sizeof(head));
size=;
memset(vis,,sizeof(vis));
for(int i=;i<=n;++i){
v[i].clear();
fa[i]=i;
}
dis[]=dep[]=;
} void add(int a,int b,int v){
point[size]=b;
val[size]=v;
nxt[size]=head[a];
head[a]=size++;
point[size]=a;
val[size]=v;
nxt[size]=head[b];
head[b]=size++;
} int find(int x){
return x==fa[x]?x:fa[x]=find(fa[x]);
} void Tarjan(int s,int pre){
for(int i=head[s];~i;i=nxt[i]){
int j=point[i];
if(j!=pre){
dis[j]=dis[s]+val[i];
dep[j]=dep[s]+;
Tarjan(j,s);
int x=find(j),y=find(s);
if(x!=y)fa[x]=y;
}
}
vis[s]=;
for(int i=;i<v[s].size();++i){
int j=v[s][i].first;
if(vis[j]){
int lca=find(j);
int id=v[s][i].second;
ans[id]=lca;
// ans[id]=dep[s]+dep[j]-2*dep[lca];
// ans[id]=dis[s]+dis[j]-2*dis[lca];
}
}
} for(int i=;i<=k;++i){
int a,b;
scanf("%d%d",&a,&b);
v[a].push_back(make_pair(b,i));
v[b].push_back(make_pair(a,i));
}
Tarjan(,);

另外,现在又有LCA用dfs序+RMQ的做法,可以实现O(nlogn)预处理,O(1)查询的LCA,基本可以完全替代倍增LCA和TarjanLCA,但是树上路径长度和树上路径最小值无法用这个来做。

 #include <bits/stdc++.h>
using namespace std; const int maxn = 2e5+;
const int maxl = ;
int vis[maxn],dep[maxn],dp[maxn][maxl]; int head[maxn],in[maxn],id[maxn];
int point[maxn],nxt[maxn],sz;
int val[maxn];
int fa[maxl][maxn]; //fa[i][j]是j点向上(不包括自己)2**i 层的父节点,dep是某个点的深度(根节点深度为0),dis是节点到根节点的距离
int n; void init(){
sz = ;
memset(head,-,sizeof(head));
memset(fa,-,sizeof(fa));
} void Pre(){
for(int k=;k+<maxl;++k){ //类似RMQ的做法,处理出点向上2的幂次的祖先。
for(int v=;v<=n;++v){
if(fa[k][v]<)fa[k+][v]=-;
else fa[k+][v]=fa[k][fa[k][v]]; //处理出两倍距离的祖先
}
}
} void dfs(int u,int p,int d,int&k){
fa[][u]=p; //当前节点的上一层父节点是传入的父节点标号
vis[k] = u;
id[u] = k;
dep[k++]=d;
for(int i = head[u];~i;i=nxt[i]){
int v = point[i];
if(v == p)continue;
dfs(v,u,d+,k);
vis[k] = u;
dep[k++]=d;
}
} void RMQ(int root){
int k = ;
dfs(root,-,,k);
int m = k;
int e= (int)(log2(m+1.0));
for(int i = ; i < m ; ++ i)dp[i][]=i;
for(int j = ; j <= e ; ++ j){
for(int i = ; i + ( << j ) - < m ; ++ i){
int N = i + (<<(j-));
if(dep[dp[i][j-]] < dep[dp[N][j-]]){
dp[i][j] = dp[i][j-];
}
else dp[i][j] = dp[N][j-];
}
}
} void add(int a,int b){
point[sz] = b;
nxt[sz] = head[a];
head[a] = sz++;
} int LCA(int u,int v){
int left = min(id[u],id[v]),right = max(id[u],id[v]);
int k = (int)(log2(right- left+1.0));
int pos,N = right - (<<k)+;
if(dep[dp[left][k]] < dep[dp[N][k]])pos = dp[left][k];
else pos = dp[N][k];
return vis[pos];
} int q; inline int get(int a,int k){
int res = a;
for(int i = ; (1ll << i ) <= k ; ++ i){
if(k&(1ll<<i)){
res = fa[i][res];
}
}
return res;
} void run(){
while(q--){
int a,b,k;
scanf("%d%d%d",&a,&b,&k);
int lca = LCA(a,b);
int num = dep[id[a]] - dep[id[lca]] + dep[id[b]] - dep[id[lca]] + ;
int up = (num - )%k;
int ans = val[a];
// printf("a : %d\n",a);
while(dep[id[a]] - dep[id[lca]] >= k){
int Id = get(a,k);
ans ^= val[Id];
// printf("a : %d\n",Id);
a = Id;
}
if(dep[id[b]] - dep[id[lca]] > up){
// printf("up: %d\n",up);
if(up == )ans^= val[b];
b = get(b,up);
while(dep[id[b]] - dep[id[lca]] >k ){
int Id = get(b,k);
ans ^= val[Id];
b = Id;
}
}
printf("%d\n",ans);
}
} int main(){
while(scanf("%d%d",&n,&q)!=EOF){ init();
for(int i = ; i < n; ++ i){
int a,b;
scanf("%d%d",&a,&b);
add(a,b);
add(b,a);
}
for(int i = ;i <= n ; ++ i)scanf("%d",&val[i]);
RMQ();
Pre();
run(); }
return ;
}