最近公共祖先(LCA)
首先来介绍下最近公共祖先(LCA)的概念
- 百度上的解释:对于有根树T的两个结点u、v,最近公共祖先LCA(T,u,v)表示一个结点x,满足x是u、v的祖先且x的深度尽可能大。
- 通俗语言:在一棵没有环的树上,每个节点肯定有其父亲节点和祖先节点,而最近公共祖先节点,就是两个节点在这棵树上深度最大的公共的祖先节点,即两个点在这棵树上距离最近的公共祖先节点。易知,在树上这两个点的最短路径唯一时,一定经过该公共祖先节点。(父亲节点也是祖先节点,另外某个节点本身也是它的祖先节点)
给出个例子加深理解,如图,3和5的最近公共祖先为1,5和6的最近公共祖先为2,2和7的最近公共祖先为2, 6和7的最近公共祖先为4。
求公共最近祖先的算法(设询问次数为q)
- 暴力(实际做题不可行):对于每个询问,遍历所有的点,时间复杂度为O(n*q)。
- Tarjan(离线)算法: 在一次遍历中把所有询问一次性解决,预处理时间复杂度O(nlogn),每次查询时间复杂度O(1),总时间复杂度是O(nlogn+q)。
- 倍增算法:利用二分两个节点同时往上走,直到相遇,预处理时间复杂度O(nlogn),每次查询时间复杂度O(logn),总时间复杂度O(nlogn+qlogn)。
- RMQ算法:留坑。
Tarjan(离线)算法
一.Tarjan算法大致实现过程
- 先选择一个节点u为根节点,从根节点开始搜索。(标记u已访问过)
- 遍历该点u的所有儿子节点v,并标记v已访问过。
- 若v还有儿子节点,对v重复ii操作,否则进入下一操作。
- 把v合并到u上(并查集)。
- 把当前的点设为u,遍历与u有询问关系的节点v。
- 如果v在之前已经被访问过,那么u和v的最近公共祖先就是v通过并查集合并后的父亲节点(注意是合并后),即当前的find(v)。
Tarjan(u) //根节点u
{
for each(u,v)
{
Tarjan(v); //v还有儿子节点
join(u,v); //把v合并到u上
vis[v]=1; //访问标记
}
for each(u,v) //遍历与u有询问关系的节点v
{
if(vis[v])
{
ans=find(v);
}
}
}
1.先取1为根节点, 发现其有两个子节点2和3,先搜索2,又发现2有两个子节点4和5,先搜索4,4也有两个子节点6和7,先搜索6,这时发现6没有子节点了,然后寻找与其有询问关系的节点,发现5和7均与6有询问关系,但都没被访问过。所以返回并标记vis[6]=1,pre[6]=4;
2.接着搜索7,发现7没有子节点,然后寻找与其有询问关系的节点,发现6与其有询问关系,且vis[6]=1,所以LCA(6,7)=find(6)=4。结束并标记vis[7]=1,pre[7]=4;
3.现在节点4已经搜完,且没有与其有询问关系的节点,vis[4]=1,pre[4]=2;
4.搜索5,发现其有子节点8,搜索8,发现8没有子节点,然后寻找与其有询问关系的节点,也没有,于是返回,且vis[5]=1,pre[8]=5;
5.节点5已经搜完,发现有两个与其有询问关系的节点6和7,且vis[6]=1,所以LCA(5,6)=find(6)=2;因为vis[7]=1,所以LCA(5,7)=find(7)=2;遍历完毕返回,标记vis[5]=1,pre[5]=2;
(find过程:pre[7]=4,pre[4]=2 ==》2 )
6.节点2已经搜完,发现有一个与其有询问关系的节点7,且vis[7]=1,故LCA(2,7)=find(7)=2。遍历完毕,标记vis[2]=1,pre[2]=1;
7.接着搜索3,没有子节点,发现有一个与其有询问关系的节点5,因为vis[5]=1,所以LCA(3,5)=find(5)=1;遍历结束,标记vis[3]=1,pre[3]=1;
(find过程:pre[5]=2,pre[2]=1 ==》1 )
8.这时返回到了节点1,它没有与之有询问关系的点了,且其pre[1]=1,搜索结束。
完成求最小公共祖先的操作。
由于LCA的题目千变万化,下面给出最基础的模板(给出一系列边用邻接表保存,把询问也用邻接表保存,只求LCA,不维护其他值)
void Tarjan(int now)
{
vis[now]=1;
for(int i=head1[now];i!=-1;i=e1[i].next)
{
int t=e1[i].t;
if(vis[t]==0)
{
Tarjan(t);
join(now,t);
}
}
for(int i=head2[now];i!=-1;i=e2[i].next)
{
int t=e2[i].t;
if(vis[t]==1)
{
e2[i].lca=find(t);
e2[i^1].lca=e2[i].lca;
}
}
}
倍增算法
我们记节点v到根的深度为depth(v)。那么如果节点w是节点u和节点v的最近公共祖先的话,让u往上走(depth(u)-depth(w))步,让v往上走(depth(v)-depth(w))步,都将走到节点w。因此,我们首先让u和v中较深的一个往上走|depth(u)-depth(v)|步,再一起一步步往上走,直到走到同一个节点,就可以在O(depth(u)+depth(v))的时间内求出LCA。
二.倍增算法的实现过程
分析刚才的算法,两个节点到达同一节点后,不论怎么向上走,达到的显然还是同一节点。利用这一点,我们就能够利用二分搜索求出到达最近公共祖先的最小步数了。
首先我们要进行预处理。对于任意的节点,可以通过fa2[v]=fa[fa[v]]得到其向上走2步到达的顶点,再利用这个信息,又可以通过fa4[v]=fa2[fa2[v]]得到其向上走4步所到的顶点。以此类推,我们可以得到其向上走2^k步所到的顶点fa[v][k],预处理的时间点复杂度为O(nlogn)。
有了k=floor(logn)以内的所有信息后,就可以进行二分所搜的,每次查询的时间复杂度为O(logn)。
三.倍增算法的代码及简要分析
void dfs(int u,int pre,int d) //预处理出每个节点的深度及父亲节点
{
fa[u][0]=pre;
depth[u]=d;
for(int i=0;i<vec[u].size();i++)
{
int v=vec[u][i];
if(v!=pre)
{
dfs(v,u,d+1);
}
}
}
void init() //预处理出每个节点往上走2^k所到的节点,超过根节点记为-1
{
dfs(root,-1,0); //root为根节点
for(int j=0;(1<<(j+1))<n;j++) //n为节点数目
for(int i=0;i<n;i++)
{
if(fa[i][j]<0) fa[i][j+1]=-1;
else fa[i][j+1]=fa[fa[i][j]][j];
}
}
int LCA(int u,int v)
{
if(depth[u]>depth[v]) swap(u,v);
int temp=depth[v]-depth[u];
for(int i=0;(1<<i)<=temp;i++) //使u,v在同一深度
{
if((1<<i)&temp)
v=fa[v][i];
}
if(v==u) return u;
for(int i=(int)log2(n*1.0);i>=0;i--) //两个节点一起往上走
{
if(fa[u][i]!=fa[v][i])
{
u=fa[u][i];
v=fa[v][i];
}
}
return fa[u][0];
}
基于RMQ的LCA算法
一、主要思路
大家都知道DFS序吧(不知道的可以先自行百度),对一棵树可以一遍DFS处理出搜索过程中进入某个点以及走出某个点的编号,ss[i]和tt[i].同时顺便得到每个点距离根节点的距离depth[i]。
那么我们回归到LCA的定义,假设我们要求a点和b点的LCA,我们要找的便是从a点走到b点过程中离根节点最近的那个点i,即i满足min(ss[a],ss[b])<=ss[i]<=max(ss[a],ss[b])且depth[i]最大的i。而这可以利用RMQ高效求得。
二.RMQ算法的代码及简要分析
int id[2*maxn]; //保存DFS时每个编号对应的节点编号
int depth[2*maxn]; //保存DFS时每个编号对应的节点深度
int ss[maxn]; //保存每个节点在DFS时第一次出现的编号
int dp[2*maxn][30]; //ST预处理时的数组,用以查询区间里深度最小的编号(DFS序编号)
void dfs(int u,int pre,int dep)
{
id[++tot]=u;
ss[u]=tot;
depth[tot]=dep;
for(int i=head[u];~i;i=e[i].next)
{
int v=e[i].v;
if(v==pre) continue;
dfs(v,u,dep+1);
id[++tot]=u;
depth[tot]=dep;
}
}
void ST(int n) //n一般取2*n-1
{
int k=(int)(log2(1.0*n));
for(int i=1;i<=n;i++) dp[i][0]=i;
for(int j=1;j<=k;j++)
for(int i=1;i+(1<<j)-1<=n;i++)
{
int a=dp[i][j-1];
int b=dp[i+(1<<(j-1))][j-1];
if(depth[a]<depth[b]) dp[i][j]=a;
else dp[i][j]=b;
}
}
int RMQ(int l,int r)
{
int k=(int)(log2(1.0*r-l+1));
int a=dp[l][k];
int b=dp[r-(1<<k)+1][k];
if(depth[a]<depth[b]) return a;
else return b;
}
int LCA(int x,int y)
{
int l=ss[x];
int r=ss[y];
if(l>r) swap(l,r);
return id[RMQ(l,r)];
}
【更新】
2017/7/30 增加LCA倍增算法,并修改原有错误
2017/9/13 增加RMQ算法。