树上倍增(LCA)


第2题     [模板] LCA 查看测评数据信息

模板题:求 LCA(最近公共祖先)。

以 1 号节点为树的根。


输入格式

第一行两个正整数 n, m,表示树的节点数和询问的个数。

接下来 n - 1 行,每行两个正整数 u, v,表示树上有一条连接节点 u 和 v 的边。

接下来 m 行,每行两个正整数 u, v,表示询问节点 u 和 v 的最近公共祖先。


对于 40% 的数据,满足 1 ≤ n, m ≤ 100。

对于 60% 的数据,满足 1 ≤ n, m ≤ 104,数据随机生成。

对于 100% 的数据,满足 1 ≤ n, m ≤ 105。


输出格式

输出共 m 行,每行一个整数表示答案。

输入/输出例子1

输入:

5 2

1 2

1 3

2 4

2 5

1 4

5 4


输出:

1

2

样例解释



 

 

闲笔:笔者于2024.7.19 重新翻回此文章,并以此做修改

详解


 

主要讲一下深度做法

思路:

 

 

 以此图为例,图中节点中心数字为深度(dep),旁边的数字为编号。我们现在要求x与y的最近公共祖先(ans,也就是1)

1.先把x,y抬到同一高度,最终有dep[x]==dep[y],即x到了x',图中蓝色地方

注意,如果此时x和y是同一个点,那么答案就出来了,就是这个点

2.x,y同时往上跳,最终到深度为2的节点,此时深度为2的节点的父亲就是所求

 

对于第1步,我们要预处理dep,这个跑一遍dfs即可。

 

但是我们要能以O(1)时间内知道x跳多少步能到什么节点,也就是预处理

一步一步暴力来看太慢了,采用倍增的思想,每次跳2^k步,可以优化很多时间,对于为什么是对的,可以看下面的解释

定 f[i][j] 表示 i节点跳2^j步,能到的节点.这样我们就可以dfs的时候处理一下了,可以理解为dp

初始化很好想,f[u][0]=fa,即一个节点的孩子节点跳2^0步可以到这个节点

转移方程:我们可以折半来想,跳2^j步换一种方式表示

2^j  =  2^(j-1)*2   =  2^(j-1) + 2^(j-1)

那么我们可以推出

f[i][j]  =  f[  f[i][j-1]  ][j-1]

 

对于第2步,我们不能直接跳到他们的LCA节点,可能会误判,注意这点之后直接做就好,具体为什么可以看下面的解释

 

模拟一下这个图求LCA;

第一步:x  : 8 -> 4

第二步:(x)4 -> 2     (y)6 -> 3

输出答案   f[2][0] = 1

 

 

一些问题和解释


 

为什么每次跳2^k步可以正好跳到目标点?为什么跳的时候k从大往小试?

我们先解释后者。

从小向大跳,5≠1+2+4,所以我们还要回溯一步,然后才能得出5=1+4

而从大向小跳,直接可以得出5=4+1。

这也可以拿二进制为例,5(101),从高位向低位填很简单,如果填了这位之后比原数大了,那我就不填,否则就填上,这个过程是很好操作的。

 

解释完后者,前者也就出来了。

任何数都可以用二进制表示,例如:13,他的二进制就是8+4+1即1101,我们从大往小去试,一定试可以得出

例子:

64>13,跳过

32>13,跳过

16>13,跳过

8<13,我们选择8,此时还差5

4<5,我们选择4,此时还差1

2>1,跳过

1<=1,我们选择1,此时凑齐了

所以,13=8+4+1

所以不用担心跳过头的情况。

 

 

为什么x,y一起跳的时候要跳到最近公共祖先点的下面一个点,也就是儿子节点?

 如果x,y想要一开始就跳到最近公共祖先,那么可能会成为公共祖先,但不是“最近”,因为无法确定那个节点是最近的

但是如果跳到最近公共祖先的儿子节点,这是很好判断的,因此此时x和y肯定不在同一个点上,就有 f[x][i] != f[y][i] ,那么一定能正确找到“最近”公共祖先

 

 

 

Code


 

#include <bits/stdc++.h>
using namespace std;
const int N=500005, M=22;

int n, m, u1, v1, root, dep[N], f[N][M];
vector<int> a[N];
void dfs(int u, int fa)
{
	dep[u]=dep[fa]+1;
	
	f[u][0]=fa;
	for (int i=1; i<=20; i++)
		f[u][i]=f[f[u][i-1]][i-1];
	
	for (int i=0; i<a[u].size(); i++)
	{
		int v=a[u][i];
		if (v!=fa) dfs(v, u);
	}
}
int lca(int x, int y)
{
	if (dep[x]<dep[y]) swap(x, y);
	
	for (int i=20; i>=0; i--)
		if (dep[f[x][i]]>=dep[y]) x=f[x][i];
	
	if (x==y) return x;
	for (int i=20; i>=0; i--)
		if (f[x][i]!=f[y][i]) x=f[x][i], y=f[y][i];
	
	return f[x][0];		
}
int main()
{
	scanf("%d%d%d", &n, &m, &root);
	for (int i=1; i<n; i++)
	{
		scanf("%d%d", &u1, &v1);
		a[u1].push_back(v1);
		a[v1].push_back(u1);
	}
	dfs(root, -1);
	while (m--)
	{
		scanf("%d%d", &u1, &v1);
		printf("%d\n", lca(u1, v1));
	}
	return 0;
}

  

 

 

 

用法/总结


 

复杂度分析


 

基于倍增,LCA函数是O(logn)的复杂度的,预处理也就是O(n)内完成,如果有q次询问,那总体就是 O(n+qlogn)

 

求树上两点的距离


 

LCA可以求树上两点间的距离,例如求A,B间的距离,就是 dep[A]+dep[B]-2*dep[lac(A, B)]   (备注:这样算是dep[root]=0的情况)

例如下图:

 算A,B的距离,就是A到根节点的距离(绿线)+B到根节点的距离(蓝线)-  AB的公共祖先到根节点的距离*2(紫线)

因为我们只需要算A,B的距离,不用算A,B到根节点的距离,所以减去AB公共祖先到根节点的距离即可,注意乘上二,因为A,B分别都要减去一次。

 

如果你根节点的dep值是1,那么算式是:(dep[x]+dep[y]-1*2)-2*(dep[lca(x, y)]-1)

例题:https://www.luogu.com.cn/problem/P10076

 

 

技巧总结


 

一些题目用LCA做,都类似于”树上容斥”,因为树上两点间路径唯一,先加总体再减重复(最近公共祖先之前即为重复)。

 

 

 

 

 

之前的文章


 

 

 

 

以下内容为之前写的了,可以不用看了,不过可以看看时间戳的做法,也挺详细的。


 

 

深度做法:

#include<bits/stdc++.h>
using namespace std;
int h[1000005],f[500005][20],n,m,s,x,y,k,dep[1000005];
struct vv{
	int r,pre;
}d[1000005];
void cun(int x,int y){
	d[++k].r=y;
	d[k].pre=h[x];
	h[x]=k;
}
void dfs(int x,int y){
	dep[x]=dep[y]+1;
	for(int i=h[x];i;i=d[i].pre){
		int j=d[i].r;
		if(j==y) continue;
		f[j][0]=x;
		for(int k=1;(1<<k)<=dep[x];k++){
			f[j][k]=f[f[j][k-1]][k-1];
		}
		dfs(j,x);
	}
}
int lca(int x,int y){
	if(dep[x]<dep[y]) swap(x,y);
	for(int i=19;i>=0;i--){
		if(dep[f[x][i]]>=dep[y]){
			x=f[x][i];
		}
	}
	if(x==y) return y;
	for(int i=19;i>=0;i--){
		if(f[x][i]!=f[y][i]){
			x=f[x][i];
			y=f[y][i];
		}
	}
	return f[x][0];
}
int main(){
	scanf("%d%d%d",&n,&m,&s);
	for(int i=1;i<n;i++){
		scanf("%d%d",&x,&y);
		cun(x,y); cun(y,x);
	}	
	dfs(s,0);
	while(m--){
		scanf("%d%d",&x,&y);
		int tem=lca(x,y);
		printf("%d\n",tem);
	}
	return 0;
}

  

 

 

解释1:https://www.luogu.com.cn/problem/solution/P3379 (第一篇题解详细解释,按深度做)

解释2:按时间戳做

#include <bits/stdc++.h>
using namespace std;

const int N=1e5+5, M=32;
int n, m, u1, v1, vis[N], f[N][M], cnt=0;
//vis[i]:节点i访问时间
//f[i][j]:从节点f,走2^j步,能到的点 vector<int> a[N]; void dfs(int u, int fa) //u:当前节点 fa:他的父亲节点 { vis[u]=++cnt; //访问时间 f[u][0]=fa; //u点走1步到父亲 for (int i=1; i<=20; i++) //2^20就行,看题目范围 f[u][i]=f[f[u][i-1]][i-1]; //拆两半,u节点跳2^i步=u节点跳2^(i-1)步+u节点跳2^(i-1)步=f [ f[u][i-1] ] [i-1] //意思是u的2^i祖先等于u的2^(i-1)祖先的2^(i-1)祖先
  //2^i = 2^(i-1) + 2^(i-1) for (int i=0; i<a[u].size(); i++) { int v=a[u][i]; if (v!=fa) dfs(v, u); } } int lca(int u, int v) { if (vis[u]>vis[v]) swap(u, v); //保证v访问时间是大的 if (u==v) return u; //别忘了加 for (int i=20; i>=0; i--)//从后往前推。原因如下
//从小向大跳,5≠1+2+4,所以我们还要回溯一步,然后才能得出5=1+4;而从大向小跳,直接可以得出5=4+1。这也可以拿二进制为例,5(101),从高位向低位填很简单,如果填了这位之后比原数大了,那我就不填,这个过程是很好操作的。 if (vis[f[v][i]]>vis[u]) //见下 解释1 v=f[v][i]; return f[v][0]; //距离u,v最近公共祖先还差一步 } int main() { scanf("%d%d", &n, &m); for (int i=1; i<n; i++) { scanf("%d%d", &u1, &v1); a[u1].push_back(v1); a[v1].push_back(u1); } dfs(1, 1); //这里定1号节点的父亲节点是自己 while (m--) { scanf("%d%d", &u1, &v1); printf("%d\n", lca(u1, v1)); } return 0; }

 解释1

 例如这张图,u,v最近公共祖先是三角型那里

有一些二分的思想(v点时间必须>u点时间,且>三角型点时间)

v点先尝试跳64步,发现不行,跳32步.....一直到跳16步,才可以比三角形大,也比u大(因为先根遍历,所以那个点时间必然>u时间)

发现可行就抬高,然后最终就一定会在三角形点的下面一个点

 

 会不会跳过头?

https://www.cnblogs.com/didiao233/p/18000595

最后一段(文章末尾,文字)

posted @ 2024-02-01 10:15  cn是大帅哥886  阅读(13)  评论(0编辑  收藏  举报