树链剖分

0.写在前面

0.1 我和树链剖分的故事(2019)

树链剖分这个东西,我在去年4月初的时候就接触了这个东西,那时候是 h y h hyh hyh(落月之神)大佬给我们讲的,那时候特别的菜,线段树都不会打,树链剖分自然听不懂
记得在去年清明节的时候照着题解打了三天
结果为什么错了呢?因为我宏定义没加括号…
然后后来就渐渐的忘掉了
知道他可以用来求 l c a lca lca,但是一直用倍增求得

0.2 我和树链剖分的故事(2020)

这几天在刷 G S S GSS GSS系列的题,做 G S S 7 GSS7 GSS7的时候要用树链剖分,然后就来重新学了一下
还不是为了更blog
后来发现树链剖分的板子其实不难qwq

1.树链剖分

1.1 前置芝士

  1. 线段树区间修改区间查询
  2. 倍增lca向上跳的思想 其实这个不重要啦,但是学树剖的应该都会lca
  3. dfs序(然后不是严格的dfs序)
  4. c++基础语法

1.2 数组定义

变量名意义
f a z [ u ] faz[u] faz[u]表示 u u u的父亲是谁
s i z [ u ] siz[u] siz[u]表示以 u u u为根的子树的大小
d e p [ u ] dep[u] dep[u]表示 u u u节点的深度
s o n [ u ] son[u] son[u]表示 u u u的重儿子是谁
d f n [ u ] dfn[u] dfn[u]表示 u u u节点重新标号后的编号
t o p [ u ] top[u] top[u]表示 u u u节点所在的重链的起始节点的编号

据说变量长度一样能带来好运

1.3 重儿子与重链

这两个东西都是树剖中新引入的一个东西
一个节点的重儿子是指这个节点的所有出边中子树大小最大的那个点(他爸爸除外)
其它的儿子统称轻儿子
重链由一个顶端的轻儿子和往下的重儿子连接而成
来张图
在这里插入图片描述

在上面那张图中,所有的加粗的边都是重边,重边链接起来的就是重链,所有标有红点的节点都是重链的起始节点
1是1-4-9-13-14的起点
2是2-6-11的七点
3是3-7的起点
5是5-5的起点
8是8-8的起点
10是10-10的起点
12是12-12的起点
每个点的重儿子也很好看出来了
1的重儿子是4
2的重儿子是6
3的重儿子是7
4的重儿子是9
6的重儿子是11
9的重儿子是13
13的重儿子是14
剩下的点都没有重儿子

弄清楚这个剩下的就很简单啦

2.树链剖分的代码实现

2.1 dfs1

树链剖分由两次 d f s dfs dfs来实现
先说 d f s 1 dfs1 dfs1,他要处理我们6个数组中的前4个
f a z faz faz, s i z siz siz, d e p dep dep, s o n son son
这个…
应该挺简单的吧
只要会树上dfs的应该都没问题吧
直接看代码

void dfs1(int u,int fa){
	faz[u]=fa;
	siz[u]=1;
	dep[u]=dep[fa]+1;//做faz,siz,dep
	RepG(i,u){
		int v=e[i].to;
		if(v==fa)continue;
		dfs1(v,u);//递归
		siz[u]+=siz[v];//更改siz
		if(siz[v]>siz[son[u]])son[u]=v;//如果v的大小比当前重儿子还要大,那么v变成重儿子
	}
}

跑完 d f s 1 dfs1 dfs1
在这里插入图片描述

2.2 dfs2

第二次 d f s dfs dfs,我们处理 d f n dfn dfn t o p top top
还是看代码吧
表达能力有限

void dfs2(int u,int _top){//_top表示现在这条重链的起始节点是什么
	dfn[u]=++tot;//dfn更新dfs序
	top[u]=_top;//更新top
	if(!son[u])return;//如果没有重儿子,说明他是叶子节点,直接return
	dfs2(son[u],_top);//优先递归重儿子
	RepG(i,u){
		int v=e[i].to;
		if(v==faz[u]||v==son[u])continue;//如果v是u的父亲,或者v是u的重儿子,都不能递归
		dfs2(v,v);//轻儿子是一个重链的开始
	}
}

跑完 d f s 2 dfs2 dfs2
在这里插入图片描述

然后,树剖就完了…
“那为什么树剖模板200行”
“因为光树剖没用,树剖还需要套上一些其他数据结构,这样就拉长了码长”
所以下面我们看一下树剖的应用

3.树剖求lca

l c a lca lca大家应该都会倍增的求吧
l c a lca lca大家应该也知道能用树剖求吧
怎么求呢?
我们不是有个 t o p top top存着呢吗
那好办啦,不停地向上跳就可以了,和倍增差不多嘛

简单看下代码:

int lca(int x,int y){
	while(top[x]!=top[y]){//如果他们不在一条链上
		if(dep[top[x]]<dep[top[y]])swap(x,y);//我们要看的是他们跳完之后哪个深度更深,所以这里不能写成$dep[x]<dep[y]
		x=faz[top[x]];//跳到top的父亲的位置上
	}
	if(dep[x]>dep[y])swap(x,y);//答案就是x和y中深度更浅的那个
	return x;
}

这个东西的复杂度最坏情况是满二叉树,复杂度是 O ( n l o g n ) O(nlogn) O(nlogn)(因为每一个节点都要跳),其实和倍增复杂度是差不多的
但是!!!
树链剖分常熟小啊
之前有个集训队大佬证了下,树剖常熟大概在0.5左右,而且树剖空间占的比倍增小啊 q a q qaq qaq

4.树链剖分模板

4.1 线段树

我们看到,有区间修改和区间查询问题
所以我们自然想到可以用带懒标记的区间修改区间查询 s e g m e n t   t r e e segment\ tree segment tree来解决
但是修改的一段可能不是连续的怎么办呢?
我们观察他的问题,都是一条链上的或者是一颗子树上的
我们观察我们树链剖分的过程,我们发现

1.一颗子树中的所有点的 d f n dfn dfn是连续的(dfs序的性质)
2.一条重链上的所有点的 d f n dfn dfn是连续的(因为我们优先递归的重儿子)

所以我们可以根据 d f n dfn dfn从1到 n n n建立线段树
a a a的位置就不对了怎么办,其实很简单,我们只需要在 d f s 2 dfs2 dfs2的时候加一句话就可以了

void dfs2(int u,int _top){
	dfn[u]=++tot;
	top[u]=_top;
	_a[tot]=a[u];//这里,_a[i]表示dfn为i的点的初始权值
	if(!son[u])return;
	dfs2(son[u],_top);
	RepG(i,u){
		int v=e[i].to;
		if(v==faz[u]||v==son[u])continue;
		dfs2(v,v);
	}
}

然后建树就和正常的线段树的建树方法一样了
这里就不多说了

void build(int u,int l,int r){
	seg[u].l=l,seg[u].r=r;
	if(l==r){seg[u].val=_a[l];return;}//根据dfn建树,初始权值是_a[l]
	int mid=l+r>>1;
	build(lc,l,mid);
	build(rc,mid+1,r);
	pushup(u);	
}

4.2 链修改

对于链修改
其实稍微改一改 l c a lca lca就可以了,还是每次往上跳,跳的时候将线段树中对应的 [ t o p [ x ] , x ] [top[x],x] [top[x],x]这一段给更新了就可以了
看代码:

void RouteModify(int x,int y,int k){
	k%=mod;//根据题目要求mod
	while(top[x]!=top[y]){//往上跳
		if(dep[top[x]]<dep[top[y]])swap(x,y);//同求lca
		update(1,dfn[top[x]],dfn[x],k);//因为top[x]~x这一段的dfn是连续的,所以他在线段树上的位置
		x=faz[top[x]];
	}
	if(dep[x]>dep[y])swap(x,y);//因为x和y最后没到一起,所以还要把这一段给更新一下
	update(1,dfn[x],dfn[y],k);
}

4.3 链查询

跟链查询差不多
也是往上跳啦

void RouteQuery(int x,int y){
	int res=0;
	while(top[x]!=top[y]){
		if(dep[top[x]]<dep[top[y]])swap(x,y);
		res+=query(1,dfn[top[x]],dfn[x]);//加上这一区间的答案
		res%=mod;//记得取模
		x=faz[top[x]];
	}
	if(dep[x]>dep[y])swap(x,y);
	res+=query(1,dfn[x],dfn[y]);//加上剩下x,y在一条重链上的答案
	res%=mod;
	printf("%d\n",res);//输出
}

4.4 子树修改与子树查询

这个更简单,其实都不用树剖,正常的dfs序也可以
区间就是 [ d f n [ x ] , d f n [ x ] + s i z [ x ] − 1 ] [dfn[x],dfn[x]+siz[x]-1] [dfn[x],dfn[x]+siz[x]1]
因为这个子树里面所有点的 d f n dfn dfn是连续的

void TreeModify(int x,int k){
	k%=mod;
	update(1,dfn[x],dfn[x]+siz[x]-1,k);	
}

void TreeQuery(int x){
	printf("%d\n",query(1,dfn[x],dfn[x]+siz[x]-1)%mod);	
}

4.5 关于时间复杂度

这道题里面
链修改和链查询的复杂度都是 O ( l o g 2 n ) O(log^2n) O(log2n)
为什么呢?
首先你往上跳就是一个 l o g log log吧,然后边跳还要边做线段树,又来一个 l o g log log
但是通常一般都达不到
子树修改子树查询就是线段树的一个 l o g log log
所以这道题的复杂度是 O ( q l o g 2 n ) O(qlog^2n) O(qlog2n)

5.完整代码

# include <cstdio>
# include <algorithm>
# include <cstring>
# include <cmath>
# include <climits>
# include <iostream>
# include <string>
# include <queue>
# include <stack>
# include <vector>
# include <set>
# include <map>
# include <cstdlib>
# include <ctime>
using namespace std;

# define Rep(i,a,b) for(int i=a;i<=b;i++)
# define _Rep(i,a,b) for(int i=a;i>=b;i--)
# define RepG(i,u) for(int i=head[u];~i;i=e[i].next)

typedef long long ll;
const int N=1e5+5;
//const int mod=1e9+7;
const double eps=1e-7;
template <typename T> void read(T &x){
	x=0;int f=1;
	char c=getchar();
	for(;!isdigit(c);c=getchar())if(c=='-')f=-1;
	for(;isdigit(c);c=getchar())x=(x<<1)+(x<<3)+c-'0';
	x*=f;
}

int n,m,rt,mod;
int a[N],_a[N];
int head[N],cnt;
int faz[N],son[N],dep[N],siz[N],dfn[N],top[N],tot;

struct Edge{
	int to,next;	
}e[N<<1];

void add(int x,int y){
	e[++cnt]=(Edge){y,head[x]},head[x]=cnt;	
}

struct segment_tree{
	int l,r,val,tag;	
}seg[N<<2];

# define lc (u<<1)
# define rc (u<<1|1)

void pushup(int u){
	seg[u].val=seg[lc].val+seg[rc].val;	
}

void pushdown(int u){
	seg[lc].val+=seg[u].tag*(seg[lc].r-seg[lc].l+1);
	seg[lc].val%=mod;
	seg[rc].val+=seg[u].tag*(seg[rc].r-seg[rc].l+1);
	seg[rc].val%=mod;
	seg[lc].tag+=seg[u].tag;
	seg[rc].tag+=seg[u].tag;
	seg[u].tag=0;	
}

void build(int u,int l,int r){
	seg[u].l=l,seg[u].r=r;
	if(l==r){seg[u].val=_a[l];return;}
	int mid=l+r>>1;
	build(lc,l,mid);
	build(rc,mid+1,r);
	pushup(u);	
}

void update(int u,int l,int r,int k){
	if(seg[u].l>=l&&seg[u].r<=r){
		seg[u].val+=k*(seg[u].r-seg[u].l+1);
		seg[u].tag+=k;
		return;
	}
	if(seg[u].tag)pushdown(u);
	int mid=seg[u].l+seg[u].r>>1;
	if(l<=mid)update(lc,l,r,k);
	if(r>mid)update(rc,l,r,k);
	pushup(u);
}

int query(int u,int l,int r){
	if(seg[u].l>=l&&seg[u].r<=r)return seg[u].val;
	if(seg[u].tag)pushdown(u);
	int mid=seg[u].l+seg[u].r>>1;
	int res=0;
	if(l<=mid)res+=query(lc,l,r);
	if(r>mid)res+=query(rc,l,r);
	res%=mod;
	return res;	
}

=============================================以上是线段树

void RouteModify(int x,int y,int k){//链修改
	k%=mod;
	while(top[x]!=top[y]){
		if(dep[top[x]]<dep[top[y]])swap(x,y);
		update(1,dfn[top[x]],dfn[x],k);
		x=faz[top[x]];
	}
	if(dep[x]>dep[y])swap(x,y);
	update(1,dfn[x],dfn[y],k);
}

void RouteQuery(int x,int y){//链查询
	int res=0;
	while(top[x]!=top[y]){
		if(dep[top[x]]<dep[top[y]])swap(x,y);
		res+=query(1,dfn[top[x]],dfn[x]);
		res%=mod;
		x=faz[top[x]];
	}
	if(dep[x]>dep[y])swap(x,y);
	res+=query(1,dfn[x],dfn[y]);
	res%=mod;
	printf("%d\n",res);
}

void TreeModify(int x,int k){//子树修改
	k%=mod;
	update(1,dfn[x],dfn[x]+siz[x]-1,k);	
}

void TreeQuery(int x){//子树修改
	printf("%d\n",query(1,dfn[x],dfn[x]+siz[x]-1)%mod);	
}

===============================================以上是各种操作

void dfs1(int u,int fa){
	faz[u]=fa;
	siz[u]=1;
	dep[u]=dep[fa]+1;
	RepG(i,u){
		int v=e[i].to;
		if(v==fa)continue;
		dfs1(v,u);
		siz[u]+=siz[v];
		if(siz[v]>siz[son[u]])son[u]=v;
	}
}

void dfs2(int u,int _top){
	dfn[u]=++tot;
	top[u]=_top;
	_a[tot]=a[u];
	if(!son[u])return;
	dfs2(son[u],_top);
	RepG(i,u){
		int v=e[i].to;
		if(v==faz[u]||v==son[u])continue;
		dfs2(v,v);
	}
}

===================================================以上是树剖

int main()
{
	memset(head,-1,sizeof(head));
	read(n),read(m),read(rt),read(mod);
	Rep(i,1,n)read(a[i]);
	Rep(i,1,n-1){
		int x,y;
		read(x),read(y);
		add(x,y),add(y,x);
	}
	dfs1(rt,0);
	dfs2(rt,rt);
	build(1,1,n);
	Rep(i,1,m){
		int opt,x,y,z;
		read(opt);
		switch(opt){
			case 1:
				read(x),read(y),read(z);
				RouteModify(x,y,z);
				break;
			case 2:
				read(x),read(y);
				RouteQuery(x,y);
				break;
			case 3:
				read(x),read(y);
				TreeModify(x,y);
				break;
			case 4:
				read(x);
				TreeQuery(x);
				break;			
		}
	}
	return 0;
}

6.树链剖分题目推荐

回头再说吧(逃

7.写在最后

树链剖分套上一些毒瘤数据结构就特别恶心

其实这次心血来潮学树剖就是因为那道GSS7

感谢
https://www.cnblogs.com/ivanovcraft/p/9019090.html 我在这里学的树链剖分
https://baike.baidu.com/item/%E6%A0%91%E9%93%BE%E5%89%96%E5%88%86/10524122?fr=aladdin 百度百科,我从这里盗了几张图

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值