Loading

「学习笔记」LCA——树上倍增

定义

首先,什么是 LCA?
LCA:最近公共祖先
祖先:从当前点到根节点所经过的点,包括他自己,都是这个点的祖先
\(A\)\(B\) 的公共祖先:同时是 \(A\)\(B\) 两点的祖先的点
\(A\)\(B\) 的最近公共祖先:深度最大的 \(A\)\(B\) 的公共祖先
树上倍增:
预处理 \(n\log_2n\)
求解 \(n\log_2n\)

原理

原理大体描述:两个点都往上找,找到的第一个相同的点,就是他们的 LCA
这里会有两个问题:
\(Q1\):若两个点深度不同,可能会错开
\(Q2\):若真一个一个往上找,时间太慢
对于 \(Q1\),如果两个点深度不同,而我们又需要它们深度相同,那就想办法让他们深度相同就行了呗,让更深的先跳到和另一个点深度一样,具体看代码
对于 \(Q2\),我们就要看标题了,很明显,用倍增可以缩减时间

代码和操作

原理讲得差不多了,是时候说怎么做了,实在不懂,代码有注释QWQ
首先,深搜一遍,目的是处理每个点的深度和 \(f_{i, j}\),深度用 \(deap\) 记录,\(f_{i, j}\) 的含义是 \(i\) 点向上跳 \(2^j\) 个点所到达的点,在此之前,先处理 \(log_i\) (跳 \(i\) 个点的 \(j\) 是多少,\(j\) 就是前面提到的 \(2^j\)\(j\))
代码:

lg[0] = -1;
for(ll i = 1; i <= n; ++i)
{
	lg[i] = lg[i >> 1] + 1;
}
void dfs(ll u, ll fat)//u 子节点 fat u的父节点 
{
	f[u][0] = fat;//u向上跳1<<0(=1)个点就是父节点 
	deap[u] = deap[fat] + 1;//深度为父节点+1 
	for(ll i = 1; i <= lg[deap[u]]; ++i)//更新f数组 
	{
		f[u][i] = f[f[u][i-1]][i-1];//倍增算法 
	}
	for(ll i = head[u]; i; i = e[i].nxt)//遍历边
	{
		ll v = e[i].v;
		if(v != fat)//判断到达的点是否是父节点(毕竟不能绕回去) 
		{
			dfs(v, u);//继续搜索 
		}
	}
}

然后,比较两点的深度,操作就是让深的跳到浅的,使得两点深度一样
代码:

if(deap[u] < deap[v])//保证u的深度比v大 
{
	u = u ^ v;
	v = u ^ v;
	u = u ^ v;//相当于swap(u,v); ^ 异或符号 
}
while(deap[u] > deap[v])//如果两点深度不同,那就让深度大的点跳到和另一个点的深度 
{
	u = f[u][lg[deap[u]-deap[v]]];//更新u
}

现在,一样深了,此时如果 \(A\)\(B\) 是同一个点了,那这个点就是他们的 \(\text{LCA}\),直接返回结束即可
否则,继续向下进行
两点同时向上跳,如果两点跳后仍不同,继续跳,同时更新值,如果相同,这里不确定该点是否是两点的 \(\text{LCA}\),因此,不更新值,将距离调小,继续跳(说白了就是不让他们相同),最后,他们肯定会跳到他们的 \(\text{LCA}\) 的孩子上(因为不让他们相等,距离又在不断减小,他们会距离 \(\text{LCA}\) 越来越近),返回当前点的父亲即可
代码:

for(ll i = lg[deap[u]]; i >= 0; i--)//继续向上跳 
{
	if(f[u][i] != f[v][i])//如果他们没碰面 
	{
		u = f[u][i],v = f[v][i];//更新数值,继续跳 
	}
}
return f[u][0];//返回 

完整代码:

#include <iostream>
#include <cstdio>
typedef long long ll;
using namespace std;
const ll N=5e5+5;
ll n, m, s, cnt;
struct edge
{
	ll u, v, nxt;
};
edge e[N << 1];//边表存树 
ll head[N], deap[N], f[N][20], lg[N];
//head 记录该点发出的最后一条边 deap 该点的深度 
//f[i][j] 第i号点向上跳(1<<j)个点后到达的点 lg 记录log,节约时间 
inline ll read()//快读模板
{
	ll x = 0;
	bool flag = false;//判断是否是负数 
	char ch = getchar();
	while(ch < '0' || ch > '9')
	{
		if(ch == '-') flag = true;
		ch = getchar();
	}
	while(ch >= '0' && ch <= '9')
	{
		x=(x << 3) + (x << 1) + (ch ^ 48);
		//(x<<3)左移,相当于x乘8,(x<<1)相当于x乘2,乘法结合律,x乘了10 
		ch = getchar();
	}
	return flag ? ~x + 1 : x;//是负数,减1取反;是正数,直接输出 
}//快读 
void add(ll u, ll v)//建边
{
	e[++cnt].u = u;//起始点 
	e[cnt].v = v;//终点 
	e[cnt].nxt = head[u];//记录边 
	head[u] = cnt;//更新最后的边 
} 
void dfs(ll u, ll fat)//u 子节点 fat u的父节点 
{
	f[u][0] = fat;//u向上跳1<<0(=1)个点就是父节点 
	deap[u] = deap[fat] + 1;//深度为父节点+1 
	for(ll i = 1; i <= lg[deap[u]]; ++i)//更新f数组 
	{
		f[u][i] = f[f[u][i-1]][i-1];//倍增算法 
	}
	for(ll i = head[u]; i; i = e[i].nxt)//遍历边
	{
		ll v = e[i].v;
		if(v != fat)//判断到达的点是否是父节点(毕竟不能绕回去) 
		{
			dfs(v, u);//继续搜索 
		}
	}
}
ll lca(ll u,ll v)
{
	if(deap[u] < deap[v])//保证u的深度比v大 
	{
		u = u ^ v;
		v = u ^ v;
		u = u ^ v;//相当于swap(u,v); ^ 异或符号 
	}
	while(deap[u] > deap[v])//如果两点深度不同,那就让深度大的点跳到和另一个点的深度 
	{
		u = f[u][lg[deap[u]-deap[v]]];//更新u
	}
	if(u == v)    return u;//如果是一个点,直接返回 
	for(ll i = lg[deap[u]]; i >= 0; i--)//继续向上跳 
	{
		if(f[u][i] != f[v][i])//如果他们没碰面 
		{
			u = f[u][i],v = f[v][i];//更新数值,继续跳 
		}
	}
	return f[u][0];//返回 
}//求lca 
int main()
{
	n = read(), m = read(), s = read();
	for(ll i = 1; i < n; ++i)
	{
		ll u, v;
		u = read(), v = read();
		add(u, v);
		add(v, u);
	}
	lg[0] = -1;
	for(ll i = 1; i <= n; ++i)
	{
		lg[i] = lg[i >> 1] + 1;//(1<<lg[i-1])先进行,然后判断是否相等
		//如果相等,就说明(1<<lg[i-1])能跳到这里  lg[i]=log2(i) 
	}
	dfs(s, 0);
	for(ll i = 1; i <= m; ++i)
	{
		ll a, b;
		a = read(), b = read();
		printf("%lld\n", lca(a,b));
	}
	return 0;
}
posted @ 2022-06-27 18:50  yi_fan0305  阅读(108)  评论(0编辑  收藏  举报