NOIP--最近公共祖先 LCA 倍增算法

树上倍增求LCA

  LCA指的是最近公共祖先(Least Common Ancestors),如下图所示:

  4和5的LCA就是2

  那怎么求呢?最粗暴的方法就是先dfs一次,处理出每个点的深度

  然后把深度更深的那一个点(4)一个点地一个点地往上跳,直到到某个点(3)和另外那个点(5)的深度一样

然后两个点一起一个点地一个点地往上跳,直到到某个点(就是最近公共祖先)两个点“变”成了一个点

  不过有没有发现一个点地一个点地跳很浪费时间?

       如果一下子跳到目标点内存又可能不支持,相对来说倍增的性价比算是很高的

  倍增的话就是一次跳2i 个点,不难发现深度差为x时,深度更深的那个点就需要跳x个点,于是可以写出这段代码

 

[cpp] view plain copy

  1. if(depth[a] < depth[b])    swap(a, b);  

  2. int c = depth[a] - depth[b];  

  3. for(int i = 0; i <= 14; i++){  

  4.     if(c & (1 << i)){  

  5.         a = up[a][i];  

  6.     }  

  7. }  

 

接下来很快就会发现一个很严重的问题:两个点按照这样跳,不能保证一定是最近的。所以倍增找lca的方法是这样的:从最大可以跳的步数开始跳(一定是2i),如果跳的到的位置一样,就不跳,如果不一样才跳,每次跳的路程是前一次的一半

 

  过程大概就像上图所示,但是执行完了这一段到的点不是最近公共祖先,但是,它们再往上跳一格,就到了

把这一段写成代码,就成了这样:

 

[cpp] view plain copy

  1. for(int i = 14; i >= 0; i--){  

  2.     if(up[a][i] != up[b][i]){  

  3.         a = up[a][i];  

  4.         b = up[b][i];  

  5.     }  

  6. }  

前面还需要加上一句特判(当a和b在同一边时,深度浅的那个点就是最近公共祖先) if(a == b)  return a;

 

好了,会求lca了,关键是怎么构造倍增数组。没有疑问的是向上跳一格就是自己的父节点

 

f[i][0] = fa[i];
这个是初值,接着可以根据这个推出来其他的,除此之外还要附上初值0,不然有可能会RE
f[i][j] = f[f[i][j - 1]][j - 1];
就是把这一段路,分成两段已经知道的
完整代码就是这样的:

 

[cpp] view plain copy

  1. Matrix<int> up;  

  2. inline void init_bz(){  

  3.     up = Matrix<int>(16, n + 1);  

  4.     memset(up.p, 0, sizeof(int) * 16 * (n + 1));  

  5.     for(int i = 1; i <= n; i++){  

  6.         up[i][0] = fa[i];  

  7.     }  

  8.     for(int j = 1; j <= 14; j++){  

  9.         for(int i = 1; i <= n; i++){  

  10.             up[i][j] = up[up[i][j - 1]][j - 1];  

  11.         }  

  12.     }  

  13. }  

注意倍增求LCA适用于询问多的情况,不然光在预处理上花的时间就已经够多了。

 

二,源代码展示

倍增算法可以在线求树上两个点的LCA,时间复杂度为nlogn
预处理:通过dfs遍历,记录每个节点到根节点的距离dist[u],深度d[u]
init()求出树上每个节点u的2^i祖先p[u][i]
求最近公共祖先,根据两个节点的的深度,如不同,向上调整深度大的节点,使得两个节点在同一层上,如果正好是祖先结束,否则,将连个节点同时上移,查询最近公共祖先。

1. DFS预处理出所有节点的深度和父节点

版本1

 

[cpp] view plain copy

  1. void dfs(int u){  

  2.     for(int i=head[u];i!=-1;i=edge[i].next){  

  3.         int to=edge[i].to;  

  4.         if(to==p[u][0])continue;  

  5.         d[to]=d[u]+1;  

  6.         dist[to]=dist[u]+edge[i].w;  

  7.         p[to][0]=u;         //p[i][0]存i的父节点  

  8.         dfs(to);   

  9.     }  

  10. }  

 

版本2

 

inline void dfs(int u)
{	int i;	for(i=head[u];i!=-1;i=next[i])  
	{  		if (!deep[to[i]])
		{			
			deep[to[i]] = deep[u]+1;
			p[to[i]][0] = u; //p[x][0]保存x的父节点为u;			dfs(to[i]);
		}
	}
}

 

2. 初始各个点的2^j祖先是谁 ,其中 2^j (j =0...log(该点深度))倍祖先,1倍祖先就是父亲,2倍祖先是父亲的父亲......。i的2^j祖先就是i的(2^(j-1))祖先的2^(j-1)祖先:

 

[cpp] view plain copy

  1. void init(){  

  2.     for(int j=1 ; (1<<j)<=n ; j++) {  

  3.         for(int i=1;i<=n;i++)  {  

  4.               p[i][j]=p[p[i][j-1]][j-1];  

  5.         }  

  6.     }  

  7. }  

 

版本2

 

void init()
{	int i,j;	//p[i][j]表示i结点的第2^j祖先
	for(j=1;(1<<j)<=n;j++)		for(i=1;i<=n;i++)			if(p[i][j-1]!=-1)
				p[i][j]=p[p[i][j-1]][j-1];//i的第2^j祖先就是i的第2^(j-1)祖先的第2^(j-1)祖先}

 

3.从深度大的节点上升至深度小的节点同层,如果此时两节点相同直接返回此节点,即lca。否则,利用倍增法找到最小深度的 p[a][j]!=p[b][j],此时他们的父亲p[a][0]即lca。

版本1:

 

[cpp] view plain copy

  1. int lca(int a,int b){  

  2.     if(d[a]>d[b])swap(a,b);          //b在下面   

  3.     int f=d[b]-d[a];                      //f是高度差  

  4.     for(int i=0;(1<<i)<=f;i++){    //(1<<i)&f找到f化为2进制后1的位置,移动到相应的位置  

  5.         if((1<<i)&f)    b=p[b][i];     //比如f=5(101),先移动2^0祖先,然后再移动2^2祖先  

  6.     }  

  7.     if(a!=b){  

  8.         for(int i=(int)log2(N);i>=0;i--){  

  9.             if(p[a][i]!=p[b][i]){           //从最大祖先开始,判断a,b祖先,是否相同  

  10.                 a=p[a][i]; b=p[b][i];     //如不相同,a b同时向上移动2^j  

  11.             }  

  12.         }  

  13.         a=p[a][0];                            //这时a的father就是LCA  

  14.     }  

  15.     return a;  

  16. }  

版本2

 

 

[cpp] view plain copy

  1. int lca(int a,int b)//最近公共祖先  

  2. {  

  3.     int i,j;  

  4.     if(deep[a]<deep[b])swap(a,b);  

  5.     for(i=0;(1<<i)<=deep[a];i++);  

  6.     i--;  

  7.     //使a,b两点的深度相同  

  8.     for(j=i;j>=0;j--)  

  9.         if(deep[a]-(1<<j)>=deep[b])  

  10.             a=p[a][j];  

  11.     if(a==b)return a;  

  12.     //倍增法,每次向上进深度2^j,找到最近公共祖先的子结点  

  13.     for(j=i;j>=0;j--)  

  14.     {  

  15.         if(p[a][j]!=-1&&p[a][j]!=p[b][j])  

  16.         {  

  17.             a=p[a][j];  

  18.             b=p[b][j];  

  19.         }  

  20.     }  

  21.     return p[a][0];  

  22. }  

 

NOIP信息学视频地址

视频地址

链接:https://pan.baidu.com/s/1tHo1DFMaDuMZAemNH60dmw 
提取码:7jgr

posted @ 2020-10-30 12:57  tianli3151  阅读(134)  评论(0编辑  收藏  举报