首先来介绍下最近公共祖先(LCA)的概念
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;idepth[v]) swap(u,v);
int temp=depth[v]-depth[u];
for(int i=0;(1<=0;i--) //两个节点一起往上走
{
if(fa[u][i]!=fa[v][i])
{
u=fa[u][i];
v=fa[v][i];
}
}
return fa[u][0];
}
大家都知道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高效求得。
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<r) swap(l,r);
return id[RMQ(l,r)];
}
【更新】
2017/7/30 增加LCA倍增算法,并修改原有错误
2017/9/13 增加RMQ算法。