树上差分与LCA
Posted 鱼竿钓鱼干
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了树上差分与LCA相关的知识,希望对你有一定的参考价值。
树上差分与LCA
可以看b站的这个视频讲的很不错强烈推荐
LCA
什么是LCA
LCA 全称 Least Common Ancester 即最近公共祖先
- 祖先:从树根到当前节点的路径中经过的点(不包括当前节点,并且因为是树,所以路径唯一)
- 公共祖先:两个节点祖先的交集部分的点
- 最近公共祖先:两个节点的公共祖先中深度最大的那个点
如何求解LCA?
首先考虑两个深度一样的点
对于两个深度相同的点x,y 他们到LCA的深度差是相同的
那么我们可以采取x,y同时向上跳,直到跳到相同的节点。但是这样是低效的,可能会被卡成O(N)。
考虑使用倍增法来优化
假设LCA到x的距离为d,那么d一定可以表示成一个二进制数
只要给这个二进制树每一位赋0/1,找到最小的d即可,相当于让节点每次跳
2
k
2^k
2k步
如何让节点每次跳
2
k
2^k
2k步?
我们可以倍增地记录当前节点的祖先,让
f
a
[
i
]
[
j
]
fa[i][j]
fa[i][j]表示
i
i
i上
2
j
2^j
2j层的祖先是谁,可以得到递推式
f
a
[
i
]
[
j
]
=
f
a
[
f
a
[
i
]
[
j
−
1
]
]
[
j
−
1
]
fa[i][j]=fa[fa[i][j-1]][j-1]
fa[i][j]=fa[fa[i][j−1]][j−1]
点
i
的
第
2
j
层
祖
先
,
是
点
i
的
2
j
−
1
层
祖
先
再
往
上
跳
2
j
−
1
层
点i的第2^j层祖先,是点i的2^{j-1}层祖先再往上跳2^{j-1}层
点i的第2j层祖先,是点i的2j−1层祖先再往上跳2j−1层
那么我们只需要计算节点向上跳的最小深度d即可
我们在计算二进制数的时候从大往小枚举,所以我们也从大到小枚举j
如果
f
a
[
x
]
[
j
]
=
=
f
a
[
y
]
[
j
]
fa[x][j]==fa[y][j]
fa[x][j]==fa[y][j],有可能跳过LCA了,先不着急跳
如果
f
a
[
x
]
[
j
]
!
=
f
a
[
y
]
[
j
]
fa[x][j]!=fa[y][j]
fa[x][j]!=fa[y][j],还没跳过LCA了,直接跳上去
如果
f
a
[
x
]
[
j
]
=
=
f
a
[
y
]
[
j
]
fa[x][j]==fa[y][j]
fa[x][j]==fa[y][j],也可能就是LCA了,但我们没有跳上去,这时候直接返回父亲节点就可以了,因为最终还是会跳上去的
for(int j=20;j>=0;j--){
if(fa[x][j]!=fa[y][j]){
x=fa[x][j],y=fa[y][j];
}
return fa[x][0]
}
那么如果深度不同呢?
我们让深度大的节点跳到和深度小的节点深度一样的地方就可以了,依旧使用倍增类似倍增的方法,每次跳
2
j
2^j
2j。(其实就是拆分二进制嘛)
int LCA(int x,int y){
if(deep[x]>deep[y])swap(x,y);//令deep[x]<deep[y]
for(int i=t;i>=0;i--)
if(deep[fa[y][i]]>=deep[x])y=fa[y][i];//把x调到和y同深度
if(x==y)return x;
for(int i=t;i>=0;i--)//让x和y同时向上走2^i步数
if(fa[x][i]!=fa[y][i]){
x=fa[x][i],y=fa[y][i];
}
return fa[x][0];
}
做LCA之前先建树然后用dfs或者bfs把每个点的深度和父亲节点找出来
例题
洛谷 P3379 【模板】最近公共祖先(LCA)
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<map>
#include<utility>
#include<set>
#include<vector>
#include<queue>
#include<stack>
using namespace std;
typedef long long LL;
typedef unsigned long long ULL;
typedef pair<int,int>PII;
#define endl '\\n'
//CHECK MULTIPLY INPUT !!!
//NEW DATA CLEAN !!!
//THINK > CODE !!!
const int N=5e5+10,M=25;
int fath[N][M],dep[N];
vector<int>root[N];
int n,m,s;
void dfs(int u,int fa){
fath[u][0]=fa;
dep[u]=dep[fa]+1;
for(auto v:root[u]){
if(v!=fa)dfs(v,u);
}
}
int LCA(int x,int y){
if(dep[x]<dep[y])swap(x,y);
while(dep[x]>dep[y])
x=fath[x][(int)log2(dep[x]-dep[y])];
if(x==y)return y;
for(int i=20;i>=0;i--){
if(fath[x][i]!=fath[y][i]){
x=fath[x][i],y=fath[y][i];
}
}
return fath[x][0];
}
int main(){
cin>>n>>m>>s;
for(int i=1;i<n;i++){
int x,y;cin>>x>>y;
root[x].push_back(y);
root[y].push_back(x);
}
dfs(s,0);
for(int j=1;j<=20;j++)
for(int i=1;i<=n;i++)
fath[i][j]=fath[fath[i][j-1]][j-1];
while(m--){
int x,y;cin>>x>>y;
cout<<LCA(x,y)<<endl;
}
return 0;
}
树上差分
按点差分
解决问题:把路径上的点都加上某个值,最后求每个点的权值
我们把路径拆成x-LCA,y-LCA
每一部分从下向上走
对于x-LCA部分,我们只需
m
a
r
k
[
x
]
+
+
,
m
a
r
k
[
f
a
[
L
C
A
]
[
0
]
]
−
−
mark[x]++,mark[fa[LCA][0]]--
mark[x]++,mark[fa[LCA][0]]−−
对于y-LCA部分,我们只需
m
a
r
k
[
y
]
+
+
,
m
a
r
k
[
f
a
[
L
C
A
]
[
0
]
]
−
−
mark[y]++,mark[fa[LCA][0]]--
mark[y]++,mark[fa[LCA][0]]−−
然后发现LCA被修改了两次,所以我们要对LCA取消一次重复的差分标记
m
a
r
k
[
L
C
A
]
−
−
,
m
a
r
k
[
f
a
[
L
C
A
]
[
0
]
]
+
+
mark[LCA]--,mark[fa[LCA][0]]++
mark[LCA]−−,mark[fa[LCA][0]]++
综上
int lca=LCA(x,y);
mark[x]++,mark[y]++;
mark[lca]--,mark[fa[lca][0]]--;
最后每个点的权值就是它子树的mark值之和
按边差分
解决问题:把x到y的路径上的每一条边加上某值,最后求每条边的值
常见方法:边权化点权(边权下放)
每个节点最多只有一个父节点,也就是向上连的边只有一条
所以我们就把一条边的边权转化成子节点的点权(根节点没有点权)
这样就转化到点差分了
对于x-y的路径还是分成x-LCA和y-LCA上
但要注意LCA的权值不变
mark[x]++,mark[y]++,mark[LCA]-=2
最后每个点的权值就是它子树的mark值之和,然后在化回边权即可(预处理的时候把每个点对应的边记录一下)
按深度差分
解决问题:在x的子树中,修改深度为dep[x]~dep[x]+k的点
把有关x的修改都记录下来,搜到x的时候,依照每个修改打上差分标记即可。
注意处理k特别大的情况,改成<=n
最后记得回溯
流程大概是:记录修改-深搜节点-打差分标记-前缀和算答案-回溯把差分标记回复
CF1076E
在这里插入代码片
以上是关于树上差分与LCA的主要内容,如果未能解决你的问题,请参考以下文章