树上差分与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][j1]][j1]
点 i 的 第 2 j 层 祖 先 , 是 点 i 的 2 j − 1 层 祖 先 再 往 上 跳 2 j − 1 层 点i的第2^j层祖先,是点i的2^{j-1}层祖先再往上跳2^{j-1}层 i2ji2j12j1
那么我们只需要计算节点向上跳的最小深度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的主要内容,如果未能解决你的问题,请参考以下文章

树上差分与LCA

[LCA 树上差分 点差分] 松鼠的新家

习题:电压(LCA&树上差分)

[LCA 树上差分边差分] 闇の連鎖

bzoj4390: [Usaco2015 dec]Max Flow(LCA+树上差分)

[填坑]树上差分 例题:[JLOI2014]松鼠的新家(LCA)