树上差分与LCA

​可以看b站的这个视频​​讲的很不错强烈推荐

LCA

什么是LCA

LCA 全称 Least Common Ancester 即最近公共祖先

  • 祖先:从树根到当前节点的路径中经过的点(不包括当前节点,并且因为是树,所以路径唯一)
  • 公共祖先:两个节点祖先的交集部分的点
  • 最近公共祖先:两个节点的公共祖先中深度最大的那个点

如何求解LCA?

首先考虑两个深度一样的点
对于两个深度相同的点x,y 他们到LCA的深度差是相同的
那么我们可以采取x,y同时向上跳,直到跳到相同的节点。但是这样是低效的,可能会被卡成O(N)。

考虑使用倍增法来优化
假设LCA到x的距离为d,那么d一定可以表示成一个二进制数
只要给这个二进制树每一位赋0/1,找到最小的d即可,相当于让节点每次跳树上差分与LCA_深度学习

如何让节点每次跳树上差分与LCA_权值_02步?
我们可以倍增地记录当前节点的祖先,让树上差分与LCA_深度学习_03表示树上差分与LCA_#include_04树上差分与LCA_权值_05层的祖先是谁,可以得到递推式
树上差分与LCA_深度学习_06
树上差分与LCA_#include_07
那么我们只需要计算节点向上跳的最小深度d即可
我们在计算二进制数的时候从大往小枚举,所以我们也从大到小枚举j

如果树上差分与LCA_#include_08,有可能跳过LCA了,先不着急跳
如果树上差分与LCA_#include_09,还没跳过LCA了,直接跳上去
如果树上差分与LCA_#include_08,也可能就是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]
}

那么如果深度不同呢?
我们让深度大的节点跳到和深度小的节点深度一样的地方就可以了,依旧使用倍增类似倍增的方法,每次跳树上差分与LCA_权值_05。(其实就是拆分二进制嘛)

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部分,我们只需树上差分与LCA_权值_12
对于y-LCA部分,我们只需树上差分与LCA_深度学习_13
然后发现LCA被修改了两次,所以我们要对LCA取消一次重复的差分标记
树上差分与LCA_深度学习_14

综上

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

在这里插入代码片