[Tarjan算法]最近公共祖先(LCA)问题求解

来源:互联网 发布:嵌入式linux终端 编辑:程序博客网 时间:2024/05/20 02:53

想了一想几个月前打的用于解LCA的Tarjan貌似弃坑就没再管它,然后虚拟机磁盘被我莫名其妙起爆了以后之前打的程序全都打了水漂就想起了被置之不理的Tarjan解LCA问题的板子,索性就把坑填上呗,毕竟我不是挖坑不填的主 明明还有一堆乱七八糟的平衡树没填
LCA就是树上两点的最近公共祖先。说这个之前,得先了解一下什么是树上两点的公共祖先。
这是一棵树
就比如上图中根节点为t[1]的树,在其上的节点t[4]和t[5]有两个公共祖先,一个是它们共有的父亲t[3],一个是它们共有的父亲的父亲t[1]。如果深度大一点,从它们最开始共有的祖先,即最近公共祖先(LCA)及它们的LCA的所有祖先都是它们的公共祖先。自然,根节点是以它为根的树上任意两点的公共祖先。
然而求公共祖先并没有什么用。我们所要的只是它们的LCA而已,也就是它们的公共祖先中深度最大的那个节点。在线算法有ST,不过在这里我求LCA用的算法是离线的Tarjan。
Tarjan算法是建立在一个深度优先搜索(DFS)上的,它把每一个点对的问题都统计完后通过对整棵树一遍DFS解决所有问题然后输出结果。这也是它较暴力算法时间复杂度大幅降低的原因。
在通过邻接表的方式(毕竟你也不知道它是几叉树)把树存储完以后,我们把要求求LCA的所有点对连接起来(自然也是邻接表存储哒)做出另一个图,就像红色线所标识的那样:
这还是一棵树
这样子我们就有了一个问题集合的无向图。然后我们就可以从根节点开始遍历整棵树,图上就是从t[1]开始。途中用f[x]的方式记录当前情况下x所在集合的代表,这个可以用并查集解决。
在DFS到t[x]时我们先初始化f[x]=x,并令vis[x]=true。按照问题的邻接表查询一下含有t[x]点的每个问题,如果点对中另一个点t[y]已经走过了(可以开个bool类型的vis数组解决此问题),那就把相应标号的问题写上解为find(y)(就是并查集find()操作啦),否则不予置理。之后我们遍历一遍与t[x]相连的树上的所有边,只要与t[x]相连的点t[z]的vis[z]=false,t[z]就必定是t[x]的儿子之一,我们就可以递归再搜索一遍t[z],搜索完后令f[z]=x,直到遍历完整棵树。
因为在把整棵树遍历的过程中,我们手推一下就能知道以节点t[x]为根的树中所有节点都遍历过之前,f[x]一定是x,所以可以得出只要是已经能解决的要求就必定是最近的公共祖先的结论。LCA问题也就解决了。
最后例行上代码:

#include<cstdio>#define maxn 500005#define maxm 1000001int f[maxn],t[maxn],nx[maxm],v[maxm],a[maxm<<1];int T[maxn],Nx[maxm],V[maxm],ans[maxn],h;int n,m,s;bool vis[maxn];int find(int x){    if(f[x]!=x)f[x]=find(f[x]);    return f[x];}void dfs(int r){int k;vis[r]=1;f[r]=r;    k=t[r];while(k){        if(vis[v[k]])ans[a[k]]=find(v[k]);        k=nx[k];    }k=T[r];while(k){        if(!vis[V[k]])dfs(V[k]),f[V[k]]=r;        k=Nx[k];    }}int main(){int i,b,e;    scanf("%d%d%d",&n,&m,&s);    for(i=1;i<n;i++){        scanf("%d%d",&b,&e);        Nx[++h]=T[b],T[b]=h,V[h]=e;        Nx[++h]=T[e],T[e]=h,V[h]=b;    }for(h=0,i=1;i<=m;i++){        scanf("%d%d",&b,&e);        nx[++h]=t[b],t[b]=h,v[h]=e,a[h]=i;        nx[++h]=t[e],t[e]=h,v[h]=b,a[h]=i;    }dfs(s);    for(i=1;i<=m;printf("%d\n",ans[i]),i++);}

The End

0 0