前言:

蓝桥杯前没有学到LCA,最后两题都用到了LCA。

无奈啊,今天补上这个

概述:

LCA即在一棵树上,找到两个结点的最近祖先(祖先可能有多个)。

主要有两种算法解决,这里讲解一下倍增dp算法找到LCA。

其基本思想是通过预处理出每个节点距离2^k个祖先的信息,并利用这些信息在查询时快速跳转到两个节点的同一深度,然后再沿着祖先链进行比较,找到它们的LCA。

具体来说,倍增LCA算法分为两个步骤:预处理和查询。预处理阶段需要对整棵树进行遍历,计算出每个节点距离2^k个祖先的距离信息。查询阶段则利用这些信息,在O(logn)的时间内找到两个节点的LCA。

在预处理阶段,我们可以使用动态规划的方式,逐层计算出节点距离2^k个祖先的距离信息。具体来说,设f[i][j]表示节点i距离2^j个祖先的距离,那么有以下递推式:

1
2
f[i][0] = fa[i]
f[i][j] = f[f[i][j-1]][j-1] + f[i][j-1]

其中fa[i]表示节点i的父亲节点。可以看出,f[i][j]可以由f[i][j-1]和f[f[i][j-1]][j-1]计算得到。这也就是“倍增”的含义。通过这个递推式,我们可以在O(nlogn)的时间内预处理出所有节点距离2

预处理

利用深搜处理这颗树,然后预处理每个节点的倍增数组

1
2
3
4
5
6
7
8
void dfs(int u,int fa){
f[u][0]=fa,dep[u]=dep[fa]+1;
for(int i=1;(1<<i)<=dep[u];i++)f[u][i]=f[[f[u][i-1]]][i-1];
for(auto &p:e[u]){
if(p!=fa)
dfs(p,u);
}
}

查询

让深度大的结点向上跳跃(这里的跳跃是大跳跃),直到同一深度。

然后两者同时跳,直到跳到同一结点结束(注意该节点不是LCA,应该是该节点的上面的第一个祖先才是LCA)

1
2
3
4
5
6
7
8
9
10
11
int lca(int u,int v){
if(dep[u]<dep[v])swap(u,v);//挑选深度更大的结点先跳跃
for(int i=20;i>=0;i--){
if(dep[f[u][i]]>=dep[v])u=f[u][i];//判断是否同一深度
}
if(u==v)return u;//如果用一深度
for(int i=20;i>=0;i--){
if(f[u][i]!=f[v][i])u=f[u][i],v=f[v][i];
}
return f[u][0];
}

树上差分

讲完lca,这里补充一下树上差分。

任意树上两个结点的距离会等于$distance=dist[u]+dist[v]-2*lca(u,v)

即从根到u结点的距离+根到v结点的距离-2*根到二者lca的距离(画图秒懂)

实战

接下来直接上两道蓝桥杯真题,快速掌握lca以及树上差分

题目一:蓝桥杯2023年第十四届省赛真题-砍树

给定一棵由 n 个结点组成的树以及 m 个不重复的无序数对 (a1, b1), (a2, b2),. . . , (am, bm),

其中 ai 互不相同,bi 互不相同,ai ≠ bj(1 ≤ i, j ≤ m)。

小明想知道是否能够选择一条树上的边砍断,使得对于每个 (ai , bi) 满足 ai和 bi 不连通,

如果可以则输出应该断掉的边的编号(编号按输入顺序从 1 开始),否则输出 -1.(完整题目点题目就可以)

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
vector<pair<int,int>>e[N];
int dep[N],f[N][20],s[N],n,m,ans=-1;
void init(int u,int fa){
dep[u]=dep[fa]+1;f[u][0]=fa;
for(int i=1;(1<<i)<=dep[u];i++)f[u][i]=f[f[u][i-1]][i-1];
for(auto &p:e[u]){
if(p.first!=fa){
init(p.first,u);
}
}
}
int lca(int u,int v){
if(dep[u]<dep[v])swap(u,v);
for(int i=20;i>=0;i--)
if(dep[f[u][i]]>=dep[v])u=f[u][i];
if(u==v)return u;
for(int i=20;i>=0;i--)
if(f[u][i]!=f[v][i])
u=f[u][i],v=f[v][i];
return f[u][0];
}
void dfs(int u,int fa){
for(auto &p:e[u]){
if (p.first!=fa) {
dfs(p.first, u);
s[u] += s[p.first];
if (s[p.first] == m)
ans = max(ans, p.second);
}
}
}
int main(){
ios::sync_with_stdio(0),cin.tie(0);
cin>>n>>m;
for(int i=1;i<n;i++){
int u,v;cin>>u>>v;
e[u].push_back({v,i});
e[v].push_back({u,i});
}
init(1,0);
for(int i=1;i<=m;i++){
int u,v;cin>>u>>v;
s[u]+=1;s[v]+=1;s[lca(u,v)]-=2;
}
dfs(1,0);
cout<<ans;
return 0;
}

题目二:蓝桥杯2023年第十四届省赛真题-景区导游

某景区一共有 N 个景点,编号 1 到 N。景点之间共有 N − 1 条双向的摆渡车线路相连,形成一棵树状结构。在景点之间往返只能通过这些摆渡车进行,需要花费一定的时间。
小明是这个景区的资深导游,他每天都要按固定顺序带客人游览其中 K 个景点:A1, A2, . . . , AK。今天由于时间原因,小明决定跳过其中一个景点,只带游客按顺序游览其中 K − 1 个景点。具体来说,如果小明选择跳过 Ai,那么他会按顺序带游客游览 A1, A2, . . . , Ai−1, Ai+1, . . . , AK, (1 ≤ i ≤ K)。
请你对任意一个 Ai,计算如果跳过这个景点,小明需要花费多少时间在景点之间的摆渡车上?(具体见题目)

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+5;
vector<pair<int,int>>e[N];
ll dep[N],f[N][21];
ll dist[N];

void dfs(int u,int fa,ll d){
dep[u]=dep[fa]+1,f[u][0]=fa,dist[u]=d;
for(int i=1;(1<<i)<=dep[u];i++)f[u][i]=f[f[u][i-1]][i-1];
for(auto &p:e[u])
if(p.first!=fa)
dfs(p.first,u,d+p.second);
}
int lca(int u,int v){
if(dep[u]<dep[v])swap(u,v);
for(int i=20;i>=0;i--)
if(dep[f[u][i]]>=dep[v])u=f[u][i];
if(u==v)return u;
for(int i=20;i>=0;i--)
if(f[u][i]!=f[v][i])u=f[u][i],v=f[v][i];
return f[u][0];
}
ll get(int u,int v){
return dist[u]+dist[v]-2*dist[lca(u,v)];
}
int main(){
int n,k;cin>>n>>k;
for(int i=1;i<n;i++){
int u,v,t;cin>>u>>v>>t;
e[u].push_back({v,t});
e[v].push_back({u,t});
}
vector<int>a(k);
for(auto&p:a)cin>>p;
dfs(1,0,0);
ll sum=0;
for(int i=1;i<k;i++)sum+=get(a[i-1],a[i]);
for(int i=0;i<k;i++){
ll ans=sum;
if(i!=0)ans-=(get(a[i],a[i-1]));
if(i!=k-1)ans-=(get(a[i],a[i+1]));
if(i!=0&&i!=k-1)ans+=get(a[i-1],a[i+1]);
cout<<ans<<" ";
}
return 0;
}