LCA
LCA,即最近公共祖先,在图论中应用比较广泛。
LCA的定义如下:给定一个有根树,若节点z同时是节点x和节点y的祖先,则称 z 是 x,y 的公共祖先;在 x,y 的所有公共祖先当中深度最大的称为 x,y 的最近公共祖先。下面给出三个最近公共祖先的例子:
显然,从上面的例子可以得出, LCA(x,y)即为 x,y 到根节点的路径的交汇点,也是 x 到 y 的路径上深度最小的节点。
向上标记法求LCA
求LCA最直接的方法,单次查询的时间复杂度最坏为 O(n)(看起来好像还挺快的,不过什么题会只有一次查询呢)
查询方式是从一个点向上搜,到和另一个点深度相同的地方就一起搜直到搜到。代码简单因为太慢了没人用我也懒得打了。
我见没人打
树上倍增求LCA(在线算法)
稍微思考一下,向上标记法之所以求LCA慢,是因为这dd一次只能爬一格,那么我们一次爬很多格不就OK了,根据二进制拆分有了这个算法。
设F(x,y)表示x的
辈父节点,就是x向上走
到的节点,如果这个节点不存在那么特判为0。
我们可以建二维数组F[n][m]
往根节点走 步就是走两个 ,这实质上是一个动态规划,深度就是此时的阶段。
有F[x][y]=f[f[x][k-1]][k-1],
就是x的 辈父节点再往上走 .
当我们要求两点的 LCA 时, 先让它们到同一高度. 这个过程我们使用二进制拆分来加速. 比如当两点高度相差 5 时, , 那么我们就让高度较小的那个节点先往上爬 =4 步, 再往上 =1 步. 此时两点即在同一高度.
如果爬到同一高度后两点相同, 显然这个点就是它们的 LCA, 直接返回即可.
如果两点不同, 就一起往上爬. 这是一个无限逼近的过程, 直到找到它们的 LCA 的子节点为止.
预处理O(nlogn),每次查询O(logn)
#include<iostream>
#include<cstdio>
#include<queue>
#include<cmath>
using namespace std;
const int N=6e5;
int n,m,s,t,tot=0,f[N][20],d[N],ver[2*N],Next[2*N],head[N];
queue<int> q;
void add(int x,int y)
{
ver[++tot]=y,Next[tot]=head[x],head[x]=tot;
}//邻接表存边操作。由于只求LCA时不关心边权,因此可以不存边权
void bfs()
{
q.push(s);
d[s]=1;//将根节点入队并标记
while(q.size())
{
int x=q.front();q.pop();//取出队头
for(int i=head[x];i;i=Next[i])
{
int y=ver[i];
if(d[y])
continue;
d[y]=d[x]+1;
f[y][0]=x;//初始化,因为y的父亲节点就是x
for(int j=1;j<=t;j++)
f[y][j]=f[f[y][j-1]][j-1];//递推f数组
q.push(y);
}
}
}
int lca(int x,int y)
{
if(d[x]>d[y])
swap(x,y);
for(int i=t;i>=0;i--)
if(d[f[y][i]]>=d[x])
y=f[y][i];//尝试上移y
if(x==y)
return x;//若相同说明找到了LCA
for(int i=t;i>=0;i--)
if(f[x][i]!=f[y][i])
{
x=f[x][i],y=f[y][i];
}//尝试上移x、y并保持它们不相遇
return f[x][0];//当前节点的父节点即为LCA
}
int main()
{
cin>>n>>m>>s;
t=log2(n)+1;
for(int i=1;i<n;i++)
{
int x,y;
scanf("%d%d",&x,&y);
add(x,y),add(y,x);
}
bfs();
while(m--)
{
int a,b;
scanf("%d%d",&a,&b);
printf("%d\n",lca(a,b));
}
return 0;
}
Tarjan求LCA(离线)
众所周知,Tarjan是精心设计的dfs,通过dfs的遍历特性巧妙的合并已查询点,从而实现寻找LCA。
有集合之间的关系所以用到并查集
1.任选一个点为根节点,从根节点开始。
2.遍历该点u所有子节点v,并标记这些子节点v已被访问过。
3.若是v还有子节点,返回2,否则下一步。
4.合并v到u上。
5.寻找与当前点u有询问关系的点v。
6.若是v已经被访问过了,则可以确认u和v的最近公共祖先为v被合并到的父亲节点a。
是不是有些抽象,那么来看我找的一个超棒的算法演示
具体实现也是从里面抄的
#include<bits/stdc++.h>
using namespace std;
int n,k,q,v[100000];
map<pair<int,int>,int> ans;//存答案
int t[100000][10],top[100000];//存储查询关系
struct node{
int l,r;
};
node s[100000];
/*并查集*/
int fa[100000];
void reset(){
for (int i=1;i<=n;i++){
fa[i]=i;
}
}
int getfa(int x){
return fa[x]==x?x:getfa(fa[x]);
}
void marge(int x,int y){
fa[getfa(y)]=getfa(x);
}
/*------*/
void tarjan(int x){
v[x]=1;//标记已访问
node p=s[x];//获取当前结点结构体
if (p.l!=-1){
tarjan(p.l);
marge(x,p.l);
}
if (p.r!=-1){
tarjan(p.r);
marge(x,p.r);
}//分别对l和r结点进行操作
for (int i=1;i<=top[x];i++){
if (v[t[x][i]]){
cout<<getfa(t[x][i])<<endl;
}//输出
}
}
int main(){
cin>>n>>q;
for (int i=1;i<=n;i++){
cin>>s[i].l>>s[i].r;
}
for (int i=1;i<=q;i++){
int a,b;
cin>>a>>b;
t[a][++top[a]]=b;//存储查询关系
t[b][++top[b]]=a;
}
reset();//初始化并查集
tarjan(1);//tarjan 求 LCA
}
U1S1 Tarjan是真的强,在学习SCC时老师就讲这个算法的潜力,现在总算是窥出冰山一角。时间复杂度 ,就是并查集的时间复杂度。