【原创】最近公共祖先
【概念与定义】
给定一颗有根树,若节点z既是节点x的祖先,也是节点y的祖先,则称z是x,y的公共祖先。在x,y的所有公共祖先中,深度最大的那个叫最近公共祖先,记为LCA(x,y)。
【算法实现】
-
暴力
如果我们要求x和y的LCA,那我们就设置两个个指针分别指向他们两个,把这两个指针一个一个节点地往上挪动,直到它们交汇于一点,这一点就是LCA。
这个算法面对大规模数据妥妥的没救,于是我们可以用倍增将其优化到O((nlogn)级别(n为节点数)。
-
向上标记法
对于x和y,我们知道它们到树根的路径只有唯一的一条,那么我们不妨先标记要查询的某一节点到树根的路径上的所有点,再对另一节点执行向树根的搜索(具体方法跟标记第一个点一样),直到找到第一个之前已经标记的那个节点,这个节点就是我们要求的LCA了。具体实现方法,我们可以使用DFS遍历整棵树。
-
树上倍增
树上乱搞算法的一种,用途十分广泛,也可以用来求解LCA问题,是暴力的优化。对于m个询问,复杂度可以达到O((n+m)logn)。
思路跟暴力完全一致,只是用倍增优化了一下,将一个一个节点挪换成 2^0、2^1、2^2···2^k 步的形式向上挪动调整,直到它们挪动到同一个节点。
具体算法实现有点麻烦,详细讲一下。
首先是初始化,我们可以预处理出节点之间的继承关系。
由于要使用到每个节点的深度我们可以开一个数组d[]来记录。
设f[x][k]为x节点的2^k辈节点,如果该节点不存在,那么f[x][k]=0,否则对于任意的k∈[1,logn],都有f[x][k]=f[f[x][k-1]][k-1]。这相当于做一个动态规划,即不断更新每个节点的父节点。
然后是LCA算法的步骤。
我们知道,如果不事先将x和y的指针挪动到同一深度,倍增就比较麻烦。于是我们先把它们用倍增优化挪到同一深度,然后再同步挪动,直到它们处于同一点。
于是我们分几个步骤求解:
- 若x的深度小于y的,交换他们的值。
- 将x上调到与y同一深度。若此时x=y,则直接返回LCA=x,已经找到。
- 将x和y同步上调,直到它们交汇前最后一步(因为直接查询f[]数组比在树中找x要方便许多)。把它们依次尝试挪动 2^logn···2^2、2^1 步,保持它们在同一深度。每走一步,若f[x][k]!=f[y][k],就令x=f[x][k],y=f[y][k],即向上调整。
- LCA就是执行完第三步的的f[x][0]。
代码如下:
//树上倍增LCA 70pnts 复杂度O((n+m)logn) #include<cstdio> #include<iostream> #include<cmath> #include<cstring> #include<ctime> #include<cstdlib> #include<algorithm> #include<queue> #include<set> #include<map> #define N 500010 using namespace std; struct tree{ int next,ver; }g[N<<2]; queue<int> q; int head[N<<2],tot,t,n,m,s;//t树的深度 int d[N<<2],f[N<<1][30];//d[]某结点处树的深度,f[x][i]第x个结点向根节点走2^i步的结点 void add(int x,int y) { g[++tot].ver=y; g[tot].next=head[x],head[x]=tot; } void reset(int x) { memset(d,0,sizeof(d)); q.push(x); d[x]=1; while(q.size()) { int index=q.front();q.pop();//index是相对当前遍历节点的父节点 for(int i=head[index];i;i=g[i].next) { int y=g[i].ver; if(d[y]) continue;//如果已经处理过了就进入下一轮循环 d[y]=d[index]+1;//深度增加 f[y][0]=index;//你爸是我 for(int j=1;j<=t;j++) f[y][j]=f[f[y][j-1]][j-1]; q.push(y); } } } int lca(int x,int y) { if(d[x]<d[y]) swap(x,y);//使得x比y深度大 for(int i=t;i>=0;i--) if(d[f[x][i]]>=d[y]) x=f[x][i];//使得x与y深度相等 if(x==y) return x; for(int i=t;i>=0;i--) if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i]; return f[x][0];//此时x和y并未更新为它们的LCA,而是刚好在它下面一个结点,于是我们返回这个结点的爸爸 } int main() { scanf("%d%d%d",&n,&m,&s); t=(int)(log(n)/log(2))+1; for(int i=1;i<=n-1;i++) { int x,y; scanf("%d%d",&x,&y); add(x,y);add(y,x); } reset(s); for(int i=1;i<=m;i++) { int x,y; scanf("%d%d",&x,&y); cout<<lca(x,y)<<endl; } return 0; }
-
Tarjan
总之,是一个很重要的算法。
Tarjan算法是向上标记法的并查集优化版本,体现了“并查集维护节点之间的关系”的重要特性。它是一个离线算法,复杂度为O(n+m)。
在DFS时,我们对如下3种节点进行标记:
- 回溯完毕的节点,标记为1
- 递归完毕而未回溯的节点,标记为2
- 没被递归的节点,标记为0
如同向上标记法的思路,我们知道遍历完的节点都被标记为第一种状态,那么当我们递归到某两个要查询的节点x和y时,如果其中一个已经回溯完毕,比如x,那么我们这时的LCA就是y向根节点走遇到的第一个标记为1的节点。而这个过程,恰恰可以用并查集优化。当一个结点获得1的标记时,就把它所在的集合跟它的父节点集合合并,这样遍历完一条支链后x的集合的代表元就是树根,遍历完下一条支链时,代表元就会是两支链的最近交点!
所以这样我们可以用并查集记录下某两次遍历产生的交点,而这个节点就是这两次遍历经过的某些x和y的LCA。
代码如下(话说我写的这个代码的标记编号好像跟上面讲的是反的==):
//Tarjan O(n+m) #include<cstdio> #include<iostream> #include<cmath> #include<cstring> #include<ctime> #include<cstdlib> #include<algorithm> #include<queue> #include<set> #include<map> #define N 500010 using namespace std; struct tree{ int next,ver; }g[N<<2]; queue<int> q; int head[N<<2],tot,t,n,m,s;//t树的深度 int fa[N<<1],v[N],lca[N],ans[N];//d[]某结点处树的深度,f[x][i]第x个结点向根节点走2^i步的结点 vector<int> query[N],query_id[N]; void add(int x,int y) { g[++tot].ver=y; g[tot].next=head[x],head[x]=tot; } void add_query(int x,int y,int id) { query[x].push_back(y),query_id[x].push_back(id); query[y].push_back(x),query_id[y].push_back(id); } int get(int x) { if(fa[x]==x) return x; return fa[x]=get(fa[x]); } void tarjan(int x) { v[x]=1;//标记这个结点被递归到 for(int i=head[x];i;i=g[i].next){//dfs整颗树 int y=g[i].ver; if(v[y]) continue; tarjan(y); fa[y]=x;//将这个结点和它的父节点合并 } for(int i=0;i<query[x].size();i++){//检查关于当前所在结点的询问 int y=query[x][i],id=query_id[x][i];//取出询问 if(v[y]==2){//如果该结点已经回溯 int lca=get(y);//当前x与y的LCA ans[id]=lca; } } v[x]=2;//回溯,标记为2 } int main() { scanf("%d%d%d",&n,&m,&s); t=(int)(log(n)/log(2))+1;//树的深度 for(int i=1;i<=n;i++){//初始化 head[i]=0,fa[i]=i,v[i]=0; query[i].clear(),query_id[i].clear(); } for(int i=1;i<=n-1;i++) { int x,y; scanf("%d%d",&x,&y); add(x,y);add(y,x); } for(int i=1;i<=m;i++) { int x,y; scanf("%d%d",&x,&y); if(x==y) ans[i]=x; else{ add_query(x,y,i);//输入询问 } } tarjan(s); for(int i=1;i<=m;i++) printf("%d\n",ans[i]); return 0; }