一起來學習樹鏈剖分吧!

~hsm~發表於2019-04-03

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()

這個函式的任務是:

  • 標記每個點的深度dep[]dep[]
  • 標記每個點的父親fa[]fa[]
  • 標記每個非葉子節點的子樹大小(含它自己)
  • 標記每個非葉子節點的重兒子編號son[]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(log2n)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(logn)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 樹

相關文章