LCA学习笔记

LCA

LCA是什么?

LCA(Least Common Ancestors)最近公共祖先
祖先的定义是对于一棵树中的一个结点,它的父亲,它父亲的父亲,它父亲的父亲的父亲… …一直回溯到全树根结点都是它的祖先。
在这里插入图片描述
如上图,深蓝色的结点都是浅蓝色结点的祖先。

由此可得:公共祖先就是对于两个或以上的结点,它们拥有的相同的那一个或几个祖先。
那么最近公共祖先,就是指这两个或以上结点的相同的祖先们中,深度最大,也就是最接近这些结点的一个祖先。
在这里插入图片描述
就像上图那样,深蓝色结点是任一浅蓝色结点的祖先,而靛蓝色结点则是浅蓝色结点的公共祖先,被红框选中的靛蓝色结点就是浅蓝色结点的最近公共祖先了。

求出LCA往往就能解决关于树上结点关系、树上两点之间路径、树链等问题,甚至通过树链剖分可以应用到树上区间问题


暴力求LCA

遵循从浅入深的原则,我们先来了解一下最朴素的算法:暴力求LCA
我们很容易知道,求LCA的精髓无非就是“向上跳”回溯根结点,直至跳到相同的根结点就能求得LCA。
由此引出一个问题:对于任意两个结点,谁先跳?
我们知道,任意选择的结点深度不一定相同,而深度相同的结点,只要同时向上跳,总会同时来到他们的最近公共祖先
再仔细想想,我们就能得出:在同时向上访问根结点之前,我们应当将两个结点回溯到相同深度的位置,然后再同时向上跳
所以我们得出暴力求LCA的算法流程:

  1. 在两个结点中,让深度较大的一个回溯根结点,直至两者深度相同
  2. 两个结点同时向上回溯根结点,直至两个结点相同即求出LCA

在这里插入图片描述
代码实现也不难:

node LCA(node x,node y)
{
	if(x.dep < y.dep) swap(x,y);//保证x的深度较深,方便代码编写
	while ( x.dep != x.dep) x = x.father;//使二者深度相同
	while ( x != y) x = x.father , y = y.father;//同时向上跳找LCA
	return x;//返回最近公共祖先
}


倍增求LCA

上面的暴力代码可简洁了,但毕竟是暴力嘛!总会有TLE的风险,所以我们就来优化啦!
这里用到的优化方法是倍增法,其要点是在回溯根结点的时候,尽可能地向上大跳,减少回溯的次数,从而减少时间复杂度。
然而在大跳的时候,还要考虑能否跳到深度相同的位置,如果直接枚举大跳减少的最大深度,那么时间复杂度将不会改变多少,所以我们就使用二进制枚举来解决这个问题。
如果用二进制枚举来规定大跳深度的选择,那么我们在每一次大跳有如下选择:向上回溯 2 0 , 2 1 , 2 2 , 2 3 , 2 4 , 2 5 … 2^0,2^1,2^2,2^3,2^4,2^5\dots 20,21,22,23,24,25个祖先,看到这里,有没有觉得很疑惑:这和普通枚举有区别吗?如果我们给大跳规模一个最大限定:一次最多能跳 1048576 1048576 1048576个祖先,那么普通枚举最坏情况要枚举 1048576 1048576 1048576次( O ( n ) O(n) O(n)),而二进制枚举最坏情况下只需枚举 20 20 20次(   O ( log ⁡ n ) \ O(\log n)  O(logn))!( 2 20 = 1048576 2^{20}=1048576 220=1048576)又因为二进制可以表示十进制的所有数,所以二进制枚举成为我们的最佳选择!
在一般情况下,我们限定大跳的规模为 2 20 2^{20} 220,除非遇上特别变态的数据范围,否则一般以 2 20 2^{20} 220为大跳上限比较好,当然,上限设置为 2 30 , 2 50 2^{30},2^{50} 230,250也是没有问题的。首先我们先预处理出二进制枚举的数组,记录好跳 2 n 2^n 2n会跳到哪一个祖先。然后同样按照暴力求LCA的算法流程做就好了。

  1. 预处理二进制枚举数组
  2. 像放砝码一样,从大规模往小规模跳跃枚举,直至深度较深的结点跳完后,深度不小于另一结点
  3. 深度相同时退出循环,同样用二进制枚举同时向上大跳,直至找到最近公共祖先为止

在这里插入图片描述
换一种码风:

//f[u][i]即表示u结点向上跳2^i个祖先后来到哪一个结点
void pre(int u,int father)//预处理 
{
	dep[u]=dep[father]+1;//深度记录
	for(int i=0;i<=19;i++)
		f[u][i+1]=f[f[u][i]][i];
	/*
		向上跳2^k <=> 向上跳2^(k-1),再向上跳2^(k-1) 
		2^(k)=2^(k-1+1)=2^(k-1)*2=2^(k-1)+2^(k-1)
	*/
	for(int i=head[u];i!=0;i=e[i].next)//邻接表存树。。。
	{
		int v=e[i].to;
		if(v==father) continue;
		f[v][0]=u;//u为i的父节点,i向上跳2^0即1步为u 
		pre(v,u);
	}
}

/*
LCA(x,y)思路:
1、使得dep[x]>=dep[y] 
2、将x向上跳至与y同一深度
	x=f[x][k] 
3、若x=y,LCA=y 
4、否则x,y同时向上跳,x=f[x][k],y=f[y][k] 
5、求得答案 
*/

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];//x向上跳 
		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];//返回答案 
}

用欧拉序列转化为 RMQ 问题

参考 OI Wiki

树链剖分求LCA

终于来到重磅环节,用树链剖分求LCA也是一种不错的优化方法,因为我们知道:在树链剖分操作之后

LCA

树边覆盖问题:树上差分算法

#10131. 「一本通 4.4 例 2」暗的连锁

#10131. 「一本通 4.4 例 2」暗的连锁

/*
如果第一步切断x,y之间的便,那么第二步必须切断附加边(x,y),才能令其分成不连通的两部分
我们称每条附加边(x,y)把树上的x,y之间的路径的每条边都覆盖了一遍

若第一步切断覆盖了0次的主要边,则第二步可以任意切断一条附加边
若第一步切断覆盖了1次的主要边,则第二步只能切断对应的附加边
若第一步切断覆盖2次或以上的主要边,则第二步无论怎么切也不能将其斩成不连通的两部分

树上差分算法:
对于每条非树边(x,y)令结点x和y的权值+1,LCA(x,y)的权值-2
最后DFS一遍,求出F(x)表示以x为根的子树中各个结点的权值和
F(x)就是x与它的父结点之间的树边的被覆盖次数

O(n+m) 
*/
#include<iostream>
#include<cstdio>
#include<algorithm>

using namespace std;
typedef long long ll;
int n,m,a[300005],f[100005][25],x,y,sum[300005],dep[300005];
int head[300005],cnt,lca;
ll ans;
struct edge{
	int nxt;
	int to;
}e[300005];

void addedge(int from,int to)
{
	cnt ++;
	e[cnt].nxt = head[from];
	e[cnt].to = to;
	head[from] = cnt;
	return ;
}

void prework(int u,int fa)
{
	dep[u] = dep[fa] + 1;
	for(int i = 0;i < 19;i ++) f[u][i+1] = f[f[u][i]][i];
	for(int i = head[u];i;i = e[i].nxt)
	{
		int v = e[i].to;
		if(v == fa) continue;
		f[v][0] = u;
		prework(v,u);
	}
	return ;
}

int GetLCA(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];
}

void dfs(int u)
{
	for(int i = head[u];i;i = e[i].nxt)
	{
		int v = e[i].to;
		if(v != f[u][0])
		{
			dfs(v);
			sum[u] += sum[v];
		}
	}
	sum[u] += a[u];
	return ;
}

int main()
{
	scanf("%d%d",&n,&m);
	for(int i = 1;i < n;i ++)
	{
		scanf("%d%d",&x,&y);
		addedge(x,y);
		addedge(y,x);
	}
	prework(1,0);
	for(int i = 1;i <= m;i ++)//树上差分算法 
	{
		scanf("%d%d",&x,&y);
		a[x] ++;a[y] ++;
		lca = GetLCA(x,y);
		a[lca] -= 2;
	}
	dfs(1);//统计F(x) 
	for(int i = 2;i <= n;i ++)//统计最后的答案 
	{
		if(sum[i] == 0) ans += m;
		if(sum[i] == 1) ans ++;
	}
	printf("%lld",ans);
	return 0;
}

P6869 [COCI2019-2020#5] Putovanje

P6869 [COCI2019-2020#5] Putovanje
初步想法:统计每条边被经过的次数,然后计算最小费用
朴素算法:枚举所有节点 O ( n ) O(n) O(n) ,求LCA O ( log ⁡ n ) O(\log n) O(logn) ,然后从起始节点一步一步跳到LCA统计每条边 O ( n ) O(n) O(n) ,时间复杂度大概是 O ( n 2 log ⁡ n ) O(n^2\log n) O(n2logn)
对于 n ≤ 200000 n\le 200000 n200000 的数据,朴素算法超时
考虑到 统计每条边被经过的次数 ⇔ \Leftrightarrow 求每个结点与其父亲结点相连的边的覆盖次数
所以使用树上差分的算法
对于每条非树边(x,y)令结点x和y的权值+1,LCA(x,y)的权值-2
最后DFS一遍,求出F(x)表示以x为根的子树中各个结点的权值和
F(x)就是x与它的父结点之间的树边的被覆盖次数
从而时间复杂度降至 O ( n log ⁡ n ) O(n\log n) O(nlogn)

#include<iostream>
#include<cstdio>
#include<algorithm>

using namespace std;
int n,fa[200005][25],dep[200005],upedge[200005];
int head[200005],cnt,lca,x,y;
long long f[200005],cnter[200005],w,z,cov[200005];
struct edge{
	int nxt;
	int to;
	long long c1;
	long long c2;
}e[500005];

void addedge(int from,int to,long long cost1,long long cost2)
{
	cnt ++;
	e[cnt].nxt = head[from];
	e[cnt].to = to;
	e[cnt].c1 = cost1;
	e[cnt].c2 = cost2;
	head[from] = cnt;
	return ;
}

void dfs(int u,int father)
{
	dep[u] = dep[father] + 1;
	for(int i = 1;i <= 20;i ++) fa[u][i] = fa[fa[u][i-1]][i-1];
	int v = 0;
	for(int i = head[u];i;i = e[i].nxt)
	{
		v = e[i].to;
		if(v == father)
		{
			upedge[u] = i;
			continue;
		}
		fa[v][0] = u;
		dfs(v,u);
	}
	return ;
}

int GetLCA(int x,int y)
{
	if(dep[x] < dep[y]) swap(x,y);
	for(int i = 20;i >= 0;i --)
	{
		if(dep[fa[x][i]] >= dep[y]) x = fa[x][i];
		if(x == y) return x;
	}
	for(int i = 20;i >= 0;i --)
	{
		if(fa[x][i] != fa[y][i])
		{
			x = fa[x][i];
			y = fa[y][i];
		}
	}
	return fa[x][0];
}

void dfs2(int u)
{
	int v = 0;
	for(int i = head[u];i;i = e[i].nxt)
	{
		v = e[i].to;
		if(v == fa[u][0]) continue;
		dfs2(v);cov[u] += cov[v];
	}
	cov[u] += cnter[u];
	return ;
}

int main()
{
	scanf("%d",&n);
	for(int i = 1;i < n;i ++)
	{
		scanf("%d%d%lld%lld",&x,&y,&z,&w);
		addedge(x,y,z,w);
		addedge(y,x,z,w);
	}
	dfs(1,0);
	for(int i = 2;i <= n;i ++)
	{
		lca = GetLCA(i-1,i);
		cnter[i-1] ++;cnter[i] ++;cnter[lca] -= 2;//树上差分
		//不赋值,做运算,否则会出错
	}
	dfs2(1);
	for(int i = 2;i <= n;i ++)
		f[i] = f[i-1]+min(cov[i]*e[upedge[i]].c1,e[upedge[i]].c2);
	printf("%lld",f[n]);
	return 0;
}

AHOI2008紧急集合 / 聚会

P4281 [AHOI2008]紧急集合 / 聚会

/*仔细思考发现题目本质
1.求使得指定的3个点联通的边集总长度最小值
2.求一个结点使得所有指定结点到这个结点的路径都没有重叠
Q1:发现总长度最小值 = 两两结点之间距离/2
Q2:发现在两两结点组成的3个LCA中,深度最深的LCA使得路径不会重叠 
*/
#include<iostream>
#include<cstdio>

using namespace std;
int n,a,b,m,x,y,z,ans,dep[500005];
int head[500005],cnt,f[500005][25],lca1,lca2,lca3,dis1,dis2,dis3,mindis;
struct edge{
	int nxt;
	int to;
}e[1000005];

void addedge(int from,int to)
{
	cnt ++;
	e[cnt].nxt = head[from];
	e[cnt].to = to;
	head[from] = cnt;
	return ;
}

void prework(int u,int fa)
{
	dep[u] = dep[fa] + 1;
	for(int i = 0;i < 20;i ++) f[u][i+1] = f[f[u][i]][i];
	for(int i = head[u];i;i = e[i].nxt)
	{
		int v = e[i].to;
		if(v == fa) continue;
		f[v][0] = u;
		prework(v,u);
	}
	return ;
}

int GetLCA(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",&n,&m);
	for(int i = 1;i < n;i ++)
	{
		scanf("%d%d",&a,&b);
		addedge(a,b);
		addedge(b,a);
	}
	prework(1,0);
	for(int i = 1;i <= m;i ++)
	{
		scanf("%d%d%d",&x,&y,&z);
		lca1 = GetLCA(x,y);
		lca2 = GetLCA(y,z);
		lca3 = GetLCA(x,z);
		mindis = dep[x] + dep[y] + dep[z] - dep[lca1] - dep[lca2] - dep[lca3];
		if(dep[lca1] >= dep[lca2] && dep[lca1] >= dep[lca3]) ans = lca1;
		if(dep[lca2] >= dep[lca1] && dep[lca2] >= dep[lca3]) ans = lca2;
		if(dep[lca3] >= dep[lca1] && dep[lca3] >= dep[lca2]) ans = lca3;
		printf("%d %d\n",ans,mindis);
	}
	
	return 0;
} 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值