一起来学习树链剖分吧!

版权声明:https://blog.csdn.net/huashuimu2003 https://blog.csdn.net/huashuimu2003/article/details/86744817

template

title

LUOGU 3384

题目描述
如题,已知一棵包含N个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作:
操作1: 格式: 1 x y z 表示将树从x到y结点最短路径上所有节点的值都加上z
操作2: 格式: 2 x y 表示求树从x到y结点最短路径上所有节点的值之和
操作3: 格式: 3 x z 表示将以x为根节点的子树内所有节点值都加上z
操作4: 格式: 4 x 表示求以x为根节点的子树内所有节点值之和

输入输出格式
输入格式:

第一行包含4个正整数N、M、R、P,分别表示树的结点个数、操作个数、根节点序号和取模数(即所有的输出结果均对此取模)。
接下来一行包含N个非负整数,分别依次表示各个节点上初始的数值。
接下来N-1行每行包含两个整数x、y,表示点x和点y之间连有一条边(保证无环且连通)
接下来M行每行包含若干个正整数,每行表示一个操作,格式如下:
操作1: 1 x y z
操作2: 2 x y
操作3: 3 x z
操作4: 4 x

输出格式:

输出包含若干行,分别依次表示每个操作2或操作4所得的结果(对P取模)

输入输出样例
输入样例#1:

5 5 2 24
7 3 7 8 0
1 2
1 5
3 1
4 1
3 4 2
3 2 2
4 5
1 5 1 3
2 1 3

输出样例#1:

2
21

说明

时空限制:1s,128M

数据规模:

对于30%的数据: N \leq 10, M \leq 10
对于70%的数据: N \leq {10}^3, M \leq {10}^3
对于100%的数据: N \leq {10}^5, M \leq {10}^5
( 其实,纯随机生成的树LCA+暴力是能过的,可是,你觉得可能是纯随机的么233 )

样例说明:

树的结构如下:
在这里插入图片描述
各个操作如下:
在这里插入图片描述
故输出应依次为2、21(重要的事情说三遍:记得取模)

analysis

首先说明,本文大致内容转自ChinHhh,我是照着这位大佬的思路学习的。

好,下面开始正式讲解。

Pre-skill

LCA,树形DP,DFS序。当然,线段树,邻接表都是要学一下的。

concept

树链剖分就是对一棵树分成几条链,把树形变为线性,然后利用数据结构(线段树、树状数组等)来维护这些链,减少处理难度。
需要处理的问题:

  • 将树从x到y结点最短路径上所有节点的值都加上z。
  • 求树从x到y结点最短路径上所有节点的值之和。
  • 将以x为根节点的子树内所有节点值都加上z。
  • 求以x为根节点的子树内所有节点值之和。
  • 重儿子:对于每一个非叶子节点,它的儿子中 以那个儿子为根的子树节点数最大的儿子 为该节点的重儿子。
  • 轻儿子:对于每一个非叶子节点,它的儿子中 非重儿子 的剩下所有儿子即为轻儿子。
    叶子节点没有重儿子也没有轻儿子(因为它没有儿子。。)。
  • 重边:一个父亲连接他的重儿子的边称为重边。
  • 轻边:剩下的即为轻边。
  • 重链:相邻重边连起来的 连接一条重儿子 的链叫重链。
    对于叶子节点,若其为轻儿子,则有一条以自己为起点的长度为1的链
    每一条重链以轻儿子为起点。

    在这里插入图片描述

dfs1()

这个函数的任务是:

  • 标记每个点的深度 d e p [ ] dep[]
  • 标记每个点的父亲 f a [ ] fa[]
  • 标记每个非叶子节点的子树大小(含它自己)
  • 标记每个非叶子节点的重儿子编号 s o n [ ] son[]
inline void dfs1(int x,int f,int deep)//x当前节点,f父亲,deep深度
{
	dep[x]=deep;//标记每个点的深度
	fa[x]=f;//标记每个点的父亲
	siz[x]=1;//标记每个非叶子节点的子树大小
	int maxson=-1;//记录重儿子的儿子数
	for (register int i=head[x];i;i=Next[i])
	{
		int y=ver[i];
		if (y==f) continue;//若为父亲则continue
		dfs1(y,x,deep+1);//dfs其儿子
		siz[x]+=siz[y];//把它的儿子数加到它身上
		if (siz[y]>maxson)//标记每个非叶子节点的重儿子编号
			son[x]=y,maxson=siz[y];
	}
}

dfs2()

这个函数的任务:

  • 标记每个点的新编号
  • 赋值每个点的初始值到新编号上
  • 处理每个点所在链的顶端
  • 处理每条链
  • 顺序:先处理重儿子再处理轻儿子
inline void dfs2(int x,int topf)//x当前节点,topf当前链的最顶端的节点
{
	id[x]=++cnt;//标记每个点的新编号
	weight[cnt]=val[x];//把每个点的初始值赋到新编号上来
	top[x]=topf;//这个点所在链的顶端
	if (!son[x]) return ;//如果没有儿子则返回
	dfs2(son[x],topf);//按先处理重儿子,再处理轻儿子的顺序递归处理
	for (register int i=head[x];i;i=Next[i])
	{
		int y=ver[i];
		if (y==fa[x] || y==son[x]) continue;
		dfs2(y,y);//对于每一个轻儿子都有一条从它自己开始的链
	}
}

solve()

Attention 重要的来了!!!
前面说到dfs2的顺序是先处理重儿子再处理轻儿子
我们来模拟一下:

在这里插入图片描述

  • 因为顺序是先重再轻,所以每一条重链的新编号是连续的
  • 因为是dfs,所以每一个子树的新编号也是连续的

现在回顾一下我们要处理的问题:

  • 处理任意两点间路径上的点权和
  • 处理一点及其子树的点权和
  • 修改任意两点间路径上的点权
  • 修改一点及其子树的点权

1、当我们要处理任意两点间路径时:
设所在链顶端的深度更深的那个点为x点

  • ans加上x点到x所在链顶端 这一段区间的点权和
  • 把x跳到x所在链顶端的那个点的上面一个点

不停执行这两个步骤,直到两个点处于一条链上,这时再加上此时两个点的区间和即可
在这里插入图片描述
这时我们注意到,我们所要处理的所有区间均为连续编号(新编号),于是想到线段树,用线段树处理连续编号区间和
每次查询时间复杂度为 O ( l o g 2 n ) O(log2n)

inline int qRange(int x,int y)
{
	int ans=0;
	while (top[x]!=top[y])//直到两个点处于一条链上
	{
		if (dep[top[x]] < dep[top[y]])//当两个点不在同一条链上
			swap(x,y);//把x点改为所在链顶端的深度更深的那个点
		res=0;
		query(1,1,n,id[top[x]],id[x]);//ans加上x点到x所在链顶端 这一段区间的点权和
		ans+=res;
		ans%=p;
		x=fa[top[x]];//把x跳到x所在链顶端的那个点的上面一个点
	}
	if (dep[x]>dep[y])//把x点深度更深的那个点
		swap(x,y);
	res=0;
	query(1,1,n,id[x],id[y]);//这时再加上此时两个点的区间和即可
	ans+=res;
	return ans%p;
}

2、处理一点及其子树的点权和:
想到记录了每个非叶子节点的子树大小(含它自己),并且每个子树的新编号都是连续的
于是直接线段树区间查询即可
时间复杂度为 O ( l o g n ) O(logn)

inline int qSon(int x)
{
	res=0;
	query(1,1,n,id[x],id[x]+siz[x]-1);//子树区间右端点为id[x]+siz[x]-1
	return res;
}

当然,区间修改就和区间查询一样的啦~~

inline void updRange(int x,int y,int k)//同上
{
	k%=p;
	while (top[x]!=top[y])
	{
		if (dep[top[x]] < dep[top[y]])
			swap(x,y);
		update(1,1,n,id[top[x]],id[x],k);
		x=fa[top[x]];
	}
	if (dep[x]>dep[y])
		swap(x,y);
	update(1,1,n,id[x],id[y],k);
}

inline void updSon(int x,int k)//同上
{
	update(1,1,n,id[x],id[x]+siz[x]-1,k);
}

Building the tree

既然前面说到要用线段树,那么按题意建树就可以啦!
不过,建树这一步当然是在处理问题之前哦~

code

#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+10;
template<typename T>inline void read(T &x)
{
	x=0;
	T f=1,ch=getchar();
	while (!isdigit(ch) && ch^'-') ch=getchar();
	if (ch=='-') f=-1, ch=getchar();
	while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48), ch=getchar();
	x*=f;
}
int tree[maxn<<2],atag[maxn<<2];//线段树数组、lazy操作
int son[maxn],id[maxn],fa[maxn];//son[]重儿子编号,id[]新编号,fa[]父亲节点
int dep[maxn],siz[maxn],top[maxn];//dep[]深度,siz[]子树大小,top[]当前链顶端节点
int n,m,r,p,cnt,res=0;//cnt dfs_clock/dfs序,res 查询答案
int ver[maxn],Next[maxn],head[maxn],len,val[maxn],weight[maxn];//val[]、weight[]初始点权数组
inline void add(int x,int y)
{
	ver[++len]=y,Next[len]=head[x],head[x]=len;
}
//-------------------------------------- 以下为线段树
inline void pushdown(int now,int x)
{
	atag[now<<1]+=atag[now];
	atag[now<<1|1]+=atag[now];
	tree[now<<1]+=atag[now]*(x-(x>>1));
	tree[now<<1|1]+=atag[now]*(x>>1);
	tree[now<<1]%=p;
	tree[now<<1|1]%=p;
	atag[now]=0;
}

inline void build(int now,int l,int r)
{
	if (l==r)
	{
		tree[now]=weight[l];
		if (tree[now]>p) tree[now]%=p;
		return ;
	}
	int mid=(l+r)>>1;
	build(now<<1,l,mid);
	build(now<<1|1,mid+1,r);
	tree[now]=(tree[now<<1]+tree[now<<1|1])%p;
}

inline void query(int now,int l,int r,int tl,int tr)
{
	if (tl<=l && r<=tr)
	{
		res+=tree[now];
		res%=p;
		return ;
	}
	else
	{
		if (atag[now])
			pushdown(now,r-l+1);
		int mid=(l+r)>>1;
		if (tl<=mid)
			query(now<<1,l,mid,tl,tr);
		if (tr>mid)
			query(now<<1|1,mid+1,r,tl,tr);
	}
}

inline void update(int now,int l,int r,int tl,int tr,int k)
{
	if (tl<=l && r<=tr)
	{
		atag[now]+=k;
		tree[now]+=k*(r-l+1);
	}
	else
	{
		if (atag[now])
			pushdown(now,r-l+1);
		int mid=(l+r)>>1;
		if (tl<=mid)
			update(now<<1,l,mid,tl,tr,k);
		if (tr>mid)
			update(now<<1|1,mid+1,r,tl,tr,k);
		tree[now]=(tree[now<<1]+tree[now<<1|1])%p;
	}
}
//---------------------------------以上为线段树
inline int qRange(int x,int y)
{
	int ans=0;
	while (top[x]!=top[y])//直到两个点处于一条链上
	{
		if (dep[top[x]] < dep[top[y]])//当两个点不在同一条链上
			swap(x,y);//把x点改为所在链顶端的深度更深的那个点
		res=0;
		query(1,1,n,id[top[x]],id[x]);//ans加上x点到x所在链顶端 这一段区间的点权和
		ans+=res;
		ans%=p;
		x=fa[top[x]];//把x跳到x所在链顶端的那个点的上面一个点
	}
	if (dep[x]>dep[y])//把x点深度更深的那个点
		swap(x,y);
	res=0;
	query(1,1,n,id[x],id[y]);//这时再加上此时两个点的区间和即可
	ans+=res;
	return ans%p;
}

inline void updRange(int x,int y,int k)//同上
{
	k%=p;
	while (top[x]!=top[y])
	{
		if (dep[top[x]] < dep[top[y]])
			swap(x,y);
		update(1,1,n,id[top[x]],id[x],k);
		x=fa[top[x]];
	}
	if (dep[x]>dep[y])
		swap(x,y);
	update(1,1,n,id[x],id[y],k);
}

inline int qSon(int x)
{
	res=0;
	query(1,1,n,id[x],id[x]+siz[x]-1);//子树区间右端点为id[x]+siz[x]-1
	return res;
}

inline void updSon(int x,int k)//同上
{
	update(1,1,n,id[x],id[x]+siz[x]-1,k);
}

inline void dfs1(int x,int f,int deep)//x当前节点,f父亲,deep深度
{
	dep[x]=deep;//标记每个点的深度
	fa[x]=f;//标记每个点的父亲
	siz[x]=1;//标记每个非叶子节点的子树大小
	int maxson=-1;//记录重儿子的儿子数
	for (register int i=head[x];i;i=Next[i])
	{
		int y=ver[i];
		if (y==f) continue;//若为父亲则continue
		dfs1(y,x,deep+1);//dfs其儿子
		siz[x]+=siz[y];//把它的儿子数加到它身上
		if (siz[y]>maxson)//标记每个非叶子节点的重儿子编号
			son[x]=y,maxson=siz[y];
	}
}

inline void dfs2(int x,int topf)//x当前节点,topf当前链的最顶端的节点
{
	id[x]=++cnt;//标记每个点的新编号
	weight[cnt]=val[x];//把每个点的初始值赋到新编号上来
	top[x]=topf;//这个点所在链的顶端
	if (!son[x]) return ;//如果没有儿子则返回
	dfs2(son[x],topf);//按先处理重儿子,再处理轻儿子的顺序递归处理
	for (register int i=head[x];i;i=Next[i])
	{
		int y=ver[i];
		if (y==fa[x] || y==son[x]) continue;
		dfs2(y,y);//对于每一个轻儿子都有一条从它自己开始的链
	}
}

int main()
{
	read(n);read(m);read(r);read(p);
	for (register int i=1;i<=n;++i)
		read(val[i]);
	for (register int i=1;i<n;++i)
	{
		int x,y;
		read(x);read(y);
		add(x,y),add(y,x);
	}
	dfs1(r,0,1);
	dfs2(r,r);
	build(1,1,n);
	while (m--)
	{
		int k,x,y,z;
		read(k);
		if (k==1)
		{
			read(x);read(y);read(z);
			updRange(x,y,z);
		}
		else if (k==2)
		{
			read(x);read(y);
			printf("%d\n",qRange(x,y));
		}
		else if (k==3)
		{
			read(x);read(y);
			updSon(x,y);
		}
		else
		{
			read(x);
			printf("%d\n",qSon(x));
		}
	}
	return 0;
}

example

NOI 2015 软件包管理器
JLOI 2014 松鼠的新家
CF 343D Water Tree
LUOGU 4315 月下”毛景树”
BZOJ 3083 遥远的国度
SDOI 2011 染色
SHOI 2012 魔法树
ZJOI 2008 树的统计
HAOI 2015 树上操作
LUOGU 3925 aaa被续
LUOGU 4114 Qtree1
LUOGU 4116 Qtree3
HEOI2016/TJOI2016 树

猜你喜欢

转载自blog.csdn.net/huashuimu2003/article/details/86744817