最近公共祖先 :
对于有根树T的两个结点u、v,最近公共祖先LCA(T,u,v)表示一个结点x,满足x是u、v的祖先且x的深度尽可能大。
通常在OI中最近公共祖先的解决办法分为在线做法和离线做法,离线做法也就是Tarjan算法,而在线做法则是倍增做法。
=========================================
Tarjan做法:利用并查集优越的时空复杂度,我们可以实现LCA问题的O(n+Q)算法,这里Q表示询问的次数。Tarjan算法基于深度优先搜索的框架,对于新搜索到 的一个结点,首先创建由这个结点构成的集合,再对当前结点的每一个子树进行搜索,每搜索完一棵子树,则可确定子树内的LCA询问都已解决。其他的LCA询 问的结果必然在这个子树之外,这时把子树所形成的集合与当前结点的集合合并,并将当前结点设为这个集合的祖先。之后继续搜索下一棵子树,直到当前结点的所 有子树搜索完。这时把当前结点也设为已被检查过的,同时可以处理有关当前结点的LCA询问,如果有一个从当前结点到结点v的询问,且v已被检查过,则由于 进行的是深度优先搜索,当前结点与v的最近公共祖先一定还没有被检查,而这个最近公共祖先的包涵v的子树一定已经搜索过了,那么这个最近公共祖先一定是v 所在集合的祖先。
#include<iostream>
#include<cstdlib>
#include<cstdio>
#include<cctype>
long long read() {
long long num = 0, f = 1;
char ch = getchar();
while(!isdigit(ch)) {
if(ch == '-') f = -1;
ch = getchar();
}
while(isdigit(ch)) num = num * 10 + ch - '0', ch = getchar();
return num * f;
}
const int maxn = 500010;
using namespace std;
int father[MAXN];
int head[MAXN];
int qhead[MAXN];
int que[MAXN];
struct Edge {
int next,to;
} edge[1000010];
struct qEdge {
int next,to,ans;
qEdge() {
ans=0;
}
} q[1000010];
int cnt;
void add(int x,int y) {
cnt++;
edge[cnt].to = y;
edge[cnt].next = head[x];
head[x] = cnt;
}
void qadd(int x,int y,int k) {
q[k].to = y;
q[k].next = qhead[x];
qhead[x] = k;
}
int find(int x) {
if(father[x] != x) father[x] = find(father[x]);
return father[x];
}
void unionn(int x,int y) {
x = find(x);
y = find(y);
father[y] = x;
}
void tarjan(int x) {
que[x] = 1;
for(int i = head[x]; i; i = edge[i].next) {
int to = edge[i].to;
if(!que[to]) {
tarjan(to);
unionn(x, to);
}
}
for(int i = qhead[x]; i; i = q[i].next) {
int to = q[i].to;
if(que[to] == 2) {
q[i].ans = find(to);
if(i%2) q[i+1].ans = q[i].ans;
else q[i-1].ans = q[i].ans;
}
}
que[x] = 2;
}
int main() {
int n = read(), m = read(), s = read();
for(int i = 1; i < n; ++i) {
int x = read(), y = read();
add(x, y);
add(y, x);
father[i] = i;
}
father[n] = n;
for(int i = 1; i <= m; ++i) {
int x = read(), y = read();
qadd(x, y, i * 2 - 1);
qadd(y, x, i * 2);
}
tarjan(s);
for(int i = 1; i <= n; ++i) printf("%d\n",q[i * 2].ans);
return 0;
}
========================================
倍增做法:每次询问O(logN)
deep[i] 表示 i节点的深度, P[i,j] 表示 i 的 2^j 倍祖先
那么就有一个递推式子 P[i,j]=P[P[i,j-1],j-1]
这样子一个O(NlogN)的预处理求出每个节点的 2^k 的祖先
然后对于每一个询问的点对(a, b)的最近公共祖先就是:
先判断是否 deep[a] > deep[b] ,如果是的话就交换一下(保证 a 的深度小于 b 方便下面的操作),然后把b 调到与a 同深度, 同深度以后再把a, b 同时往上调(dec(j)) 调到有一个最小的j 满足P[a,j] != P[b,,j] (a b 是在不断更新的), 最后再把 a, b 往上调 (a = P[a,0], b = P[b,0]) 一个一个向上调直到a = b, 这时 a or b 就是他们的最近公共祖先。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
using namespace std;
const int MAXN=1000001;
int n,m,root;
struct node
{
int u;
int v;
int next;
}edge[MAXN];
int num=1;
int head[MAXN];
int deep[MAXN];
int f[MAXN][20];
void edge_add(int x,int y)
{
edge[num].u=x;
edge[num].v=y;
edge[num].next=head[x];
head[x]=num++;
}
void build_tree(int p)
{
for(int i=head[p];i!=-1;i=edge[i].next)
{
int will=edge[i].v;
if(deep[will]==0)
{
deep[will]=deep[p]+1;
f[will][0]=p;
build_tree(will);
}
}
}
void initialize_step()
{
for(int i=1;i<=19;i++)
for(int j=1;j<=n;j++)
f[j][i]=f[f[j][i-1]][i-1];
}
int LCA(int x,int y)
{
if(deep[x]<deep[y])swap(x,y);
for(int i=19;i>=0;i--)
if(deep[f[x][i]]>=deep[y]) x=f[x][i];
if(x==y)return y;
for(int i=19;i>=0;i--)
if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i];
return f[x][0];
}
void read(int & x)
{
char c=getchar();x=0;
while(c<'0'||c>'9')c=getchar();
while(c>='0'&&c<='9')x=x*10+c-48,c=getchar();
}
int main(){
read(n);read(m);read(root);
for(int i=1;i<=n;i++)head[i]=-1;
for(int i=1;i<=n-1;i++){
int x,y;
read(x);read(y);
edge_add(x,y);
edge_add(y,x);
}
deep[root]=1;
build_tree(root);
initialize_step();
for(int i=1;i<=m;i++){
int x,y;
read(x);read(y);
printf("%d\n",LCA(x,y));
}
return 0;
}