最近公共祖先:设结点x是u,v的祖先并且x的深度是u,v所有共同祖先中最大的,则称x是u,v的最近公共祖先;
LCA的求解算法有三种:
一.tarjan算法求解LCA
这是一种离线算法,基于dfs和并查集,时间复杂度为O(nlogn+q);
算法的基本步骤:
(1),从根开始dfs
(2),如果当前点u还有儿子v,那么递归进行此算法,并且将v合并到u的并查集里面;
(3),遍历和当前点u相关的询问结点v,如果v已经访问过了,那么LCA(u,v) = v在并查集中的祖先;
理解一下就是:考虑查询的两个结点,无非只有两种关系:
(1),在同一条树链上(2),在两棵子树上
对于第一种情况,假设u是v的祖先,很容易知道LCA(u,v)=u,而在这个算法中,我们dfs肯定先遍历到u,再遍历到v时,u已经遍历过,所以二者的LCA=father[u] = u;
对于第二种情况,u,v所在的子树的分叉起点是LCA(u,v),假设先遍历到的点是v,那么遍历u时,有father[v] 等于 u,v所在的子树的分叉起点,也就是LCA(u,v)。反过来看,由点u开始分叉出的x棵子树,这x棵子树可以看成x个集合,分别从任意两个集合中各选一个点,这两个点的LCA就是u,所以用到了并查集。
关键的三步就是递归的过程中标记访问过的顶点,回溯的过程中将当前节点设置为其儿子的父亲,回溯完毕后再更新当前节点和其他节点的LCA;
例题:poj1330
//#include<bits/stdc++.h>
#include<queue>
#include <cmath>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#define mod (1000000007)
#define middle (l+r)>>1
#define lowbit(x) (x&(-x))
#define lson (rt<<1)
#define rson (rt<<1|1)
typedef long long ll;
typedef unsigned long long ull;
typedef long double ld;
const int inf_max = 0x3f3f3f3f;
const ll Linf = 9e18;
const int maxn = 1e4 + 10;
const long double E = 2.7182818;
const double eps=0.0001;
using namespace std;
inline int read()
{
int f=1,res=0;
char ch=getchar();
while(ch<'0'||ch>'9') { if(ch=='-') f=-1; ch=getchar(); }
while(ch>='0'&&ch<='9') { res=res*10+ch-'0' ; ch=getchar(); }
return f*res;
}
struct EDG {
int v,nxt;
}edge[maxn];
int cnt,n,head[maxn],vis[maxn],fa[maxn],qa,qb,ans,in[maxn];
void Initial() {
memset(head,-1,sizeof(head));
memset(vis,0,sizeof(vis));
for(int i = 0;i <= n; ++i) fa[i] = i,vis[i] = 0,in[i] = 0;
cnt = 0;
}
void add_edge(int u,int v) {
edge[cnt].v = v;
edge[cnt].nxt = head[u];
head[u] = cnt++;
}
int Find(int x) {
if(fa[x] == x) return fa[x];
return fa[x] = Find(fa[x]);
}
void tarjan(int now) {
int fnow = Find(now);
vis[now] = 1;
for(int i = head[now]; ~i; i = edge[i].nxt) {
int v = edge[i].v;
tarjan(v);
int fv = Find(v);
fa[fv] = fnow;
}
if(now == qa) {
if(vis[qb]) {
ans = Find(qb);
return ;
}
}else if(now == qb) {
if(vis[qa]) {
ans = Find(qa);
return ;
}
}
}
int main()
{
int T;
scanf("%d",&T);
while(T--) {
scanf("%d",&n);
Initial();
for(int i = 1;i < n; ++i) {
int u,v;
scanf("%d%d",&u,&v);
add_edge(u,v);
in[v]++;
}
scanf("%d%d",&qa,&qb);
int start;
for(int i = 1; i <= n; ++i) {
if(in[i] == 0) {
start = i;
break;
}
}
tarjan(start);
cout<<ans<<endl;
}
return 0;
}
二、倍增算法
这是一种在线算法,预处理时间复杂度O(nlogn),查询O(logn),总的是O(nlogn + qlogn);
首先预处理出每个点的深度depth数组和结点i的第2^j个祖先f[i][j],比如一个结点的父亲就是该节点的第 2^0 = 1 个祖先;有了这两个信息后,从LCA的定义上来看,求u,v的LCA,在分别把u,v向根移动的过程中,二者第一次相遇的点就是他们的最近公共祖先,所以可以考虑模拟这种移动的方式,直接暴力对每两个点来dfs时间就炸了,所以需要一些优化,在不改变结果的条件下增大每次向上移动的距离;
求解f数组:递推式f[i][j] = f[f[i][j-1][j-1],设t1是 i 的第 2 ^ (j-1)个祖先,t2是t1的第 2 ^ (j-1)个祖先,那么t2就是 i 的第 2 ^ (j-1)+2 ^ (j-1) = 2 ^ j 个祖先。
求解LCA(u,.v):首先将u,v移到同一个深度,这样在用f数组的时候才能保证向上移动后的深度也一样。由于我们记录了结点的第2 ^ k个祖先是谁,所以在移动的时候,我们只要枚举k(0=<k<=log2(结点总数)),如果u,v的第2 ^ k个祖先一样,都等于x,那么说明x可能是u,v的LCA,但也可能不是,因为x可能是LCA(u,v)的祖先,所以我们不能移动,只有当u,v的第2 ^ k个祖先不一样时,那么我们就可以把u,v移动上去,直到k=0的时候,得到的就是LCA(u,v)。
例题
//#include<bits/stdc++.h>
#include<queue>
#include <cmath>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#define mod (1000000007)
#define middle (l+r)>>1
#define lowbit(x) (x&(-x))
#define lson (rt<<1)
#define rson (rt<<1|1)
typedef long long ll;
typedef unsigned long long ull;
typedef long double ld;
const int inf_max = 0x3f3f3f3f;
const ll Linf = 9e18;
const int maxn = 4e4 + 10;
const long double E = 2.7182818;
const double eps=0.0001;
using namespace std;
inline int read()
{
int f=1,res=0;
char ch=getchar();
while(ch<'0'||ch>'9') { if(ch=='-') f=-1; ch=getchar(); }
while(ch>='0'&&ch<='9') { res=res*10+ch-'0' ; ch=getchar(); }
return f*res;
}
int dis[maxn],n,m,head[maxn],vis[maxn],cnt,f[maxn][20],dep[maxn];
struct node{
int v,nxt,val;
}edge[maxn<<1];
void Initial() {
memset(head,-1,sizeof(head));
memset(f,-1,sizeof(f));
cnt = 0;
memset(vis,0,sizeof(vis));
}
void add_edge(int u,int v,int val) {
edge[cnt].v = v;
edge[cnt].val = val;
edge[cnt].nxt = head[u];
head[u] = cnt++;
}
void dfs(int now,int pre,int depth) {
//printf("%d %d\n",now,pre);
dep[now] = depth;
f[now][0] = pre;
for(int i = head[now]; ~i ; i = edge[i].nxt) {
int v = edge[i].v,val = edge[i].val;
if(v == pre) continue;
dis[v] = dis[now] + val;
dfs(v,now,depth + 1);
}
}
void pre_handle() {
dis[1] = 0;
dfs(1,-1,1);
for(int i = 1; (1 << i) <= n; ++i) {
for(int j = 1; j <= n; j++) {
if(f[j][i - 1] > 0) f[j][i] = f[f[j][i-1]][i-1];//如果j的2^(i-1)次方祖先存在才更新f[j][i]
}
}
}
int getdis(int u,int v,int ances) {
int t1 = dis[u] - dis[ances],t2 = dis[v] - dis[ances];
return t1 + t2;
}
int LCA(int u,int v) {
if(dep[u] < dep[v]) swap(u,v);
//调至同一深度
int tmp = dep[u] - dep[v];
for(int i = 0;(1 << i) <= tmp; ++i) {
if((1 << i) & tmp) {
u = f[u][i];
}
}
if(u == v) return v;
//u,v同时向上找
for(int i = (int)log2(n * 1.0);i >= 0; --i) {
if(f[u][i] != f[v][i]) u = f[u][i],v = f[v][i];
}
return f[v][0];
}
int main()
{
int T;
scanf("%d",&T);
while(T--) {
scanf("%d%d",&n,&m);
Initial();
for(int i = 1;i < n; ++i) {
int u,v,val;
scanf("%d%d%d",&u,&v,&val);
add_edge(u,v,val),add_edge(v,u,val);
}
pre_handle();
for(int i = 1;i <= m; ++i) {
int u,v;
scanf("%d%d",&u,&v);
printf("%d\n",getdis(u,v,LCA(u,v)));
}
}
return 0;
}
三.转化为rmq求解LCA
这也是一个在线算法,其时间复杂度和rmq一样为O(nlogn+q),只是多了点空间的花费。带图讲解
该算法需要记录下dfs时的信息:
oula数组:记录欧拉序列的数组
dep数组;记录深度序列的数组
firpos数组:每个点在欧拉序列中第一次出现的位置
有了这些数组,我们可以将dfs时的信息记录下来成为一个一维的数组,并且维护dep数组的区间最小值就可对应出LCA(u,v);
在从u,v的简单路径中,我们一定只会经历一次u,v的LCA,那么我们在深度序列的u,v第一次出现的位置之间来查询深度数组,其中最小的值就是LCA(u,v)所处的深度;
这里dp[i][j]数组维护的是区间[i,i + 2 ^ j - 1]中最小值的位置,以便在欧拉序列中查询到u,v的LCA;
例题同上
//#include<bits/stdc++.h>
#include<queue>
#include <cmath>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#define mod (1000000007)
#define middle (l+r)>>1
#define lowbit(x) (x&(-x))
#define lson (rt<<1)
#define rson (rt<<1|1)
typedef long long ll;
typedef unsigned long long ull;
typedef long double ld;
const int inf_max = 0x3f3f3f3f;
const ll Linf = 9e18;
const int maxn = 5e4 + 10;
const long double E = 2.7182818;
const double eps=0.0001;
using namespace std;
inline int read()
{
int f=1,res=0;
char ch=getchar();
while(ch<'0'||ch>'9') { if(ch=='-') f=-1; ch=getchar(); }
while(ch>='0'&&ch<='9') { res=res*10+ch-'0' ; ch=getchar(); }
return f*res;
}
int dis[maxn],n,m,head[maxn],cnt,dep[maxn<<1],dp[maxn<<1][30],firpos[maxn],num,oula[maxn<<1];
struct node{
int v,nxt,val;
}edge[maxn<<1];
void Initial() {
memset(head,-1,sizeof(head));
cnt = num = 0;
}
void add_edge(int u,int v,int val) {
edge[cnt].v = v;
edge[cnt].val = val;
edge[cnt].nxt = head[u];
head[u] = cnt++;
}
void dfs(int now,int pre,int depth) {
firpos[now] = ++num;oula[num] = now;dep[num] = depth;
for(int i = head[now]; ~i ; i = edge[i].nxt) {
int v = edge[i].v,val = edge[i].val;
if(v == pre) continue;
dis[v] = dis[now] + val;
dfs(v,now,depth + 1);
oula[++num] = now,dep[num] = depth;
}
}
void ST() {
memset(dp,-1,sizeof(dp)); //dp数组保存区间中最小值的位置
for(int i = 1;i <= num; ++i) dp[i][0] = i;
for(int j = 1;(1 << j) <= num; ++j) {
for(int i = 1;i <= num; ++i) {
if(i + (1 << (j - 1)) > num) break;
int a = dp[i][j - 1],b = dp[i + (1 << (j - 1))][j - 1];
dp[i][j] = dep[a] < dep[b] ? a : b;
}
}
}
int getdis(int u,int v,int ances) {
int t1 = dis[u] - dis[ances],t2 = dis[v] - dis[ances];
return t1 + t2;
}
int LCA(int u,int v) {
int ql = min(firpos[v],firpos[u]),qr = max(firpos[v],firpos[u]),k = (int)log2((double)(qr + 1 - ql));
int a = dp[ql][k],b = dp[qr - (1 << k) + 1][k];
if(dep[a] < dep[b]) return oula[a];
else return oula[b];
}
int main()
{
int T;
scanf("%d",&T);
while(T--) {
scanf("%d%d",&n,&m);
Initial();
for(int i = 1;i < n; ++i) {
int u,v,val;
scanf("%d%d%d",&u,&v,&val);
add_edge(u,v,val),add_edge(v,u,val);
}
dfs(1,-1,1);
ST();
for(int i = 1;i <= m; ++i) {
int u,v;
scanf("%d%d",&u,&v);
printf("%d\n",getdis(u,v,LCA(u,v)));
}
}
return 0;
}