「学习笔记」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;
}