樹鏈剖分講解


樹鏈剖分入門講解


問題導入


當我們做題目的時候,往往會有一些題目是給定一顆樹,並對這顆樹做一堆蛇皮怪物般的操作的。

比如:  
1.詢問x到y路徑上的最小最大最¥#%¥#@¥@#¥值  
2.詢問x到y路徑上的xor,和,乘@!#¥@#¥%¥#值  
3.純模擬是過不了的且往往與lca掛鈎  
4.沒有動態的加邊刪邊23333  
5.往往下面還要接一個線段樹

那么,我們要怎么做呢?
這里提供一種思想,就是把樹拆掉:
輕重邊為基礎的拆邊,把一顆樹拆成大大小小的幾條鏈放到類似於常用的線段樹里面加以操作。


輕重邊


在樹鏈剖分里,我們定義如下規則
1.重兒子:當前節點的兒子節點中子樹大小最大的那一個 
2.輕兒子:當前節點除了重兒子以外的所有節點

那么

重鏈即為重兒子連成的鏈,輕鏈即為輕兒子連成的鏈。

我們可以通過這張圖看到如下分鏈解釋

	節點1的兒子(2,3)中2的子樹大小更大所以選取2為重兒子。  
	節點6的兒子(7,9)中7的子樹大小更大所以選取7為重兒子。

但是並不是輕兒子后面就一定是一直是輕鏈的,比如節點4后面就又接了一條重鏈。
我們可以這樣理解,當一個節點選取了一個重兒子以后,它其他的輕兒子節點就重新開始,按照之前的規則選擇它的輕重兒子。

我們可以觀察到,重鏈上的任意兩點之間的路徑是不是都在這條重鏈上,可以化成鏈直接在線段樹上調用了。

且還有如下兩個性質

1.輕邊(u,v)中, size(u)≤ size(u/2)  
2.從根到某一點的路徑上,不超過logn條輕鏈和不超過logn條重鏈。  
(蒟蒻表示並不會證)

其實我們在這里還能發現如果要統計一個點的輕重兒子,是不是還能順便處理出它的子樹size。
那么我們要怎么處理出重兒子和輕兒子呢?DFS序

void dfs1(int x) //這個是用來求一個節點的子樹大小的,即順便求出輕重兒子
{ 
	size[x]=1; //當前這個點本身大小為1
	for(int i=head[x];i;i=e[i].next) 
	{ 
		int v=e[i].to; 
		if(!dep[v]) 
		//其實這里的if里面也可以這么寫if(v!=fa[x])
		{	 	
			dep[v]=dep[x]+1; 
			fa[v]=x;//一般都是雙向邊,然后是為了跳lca
			dfs1(v);
			size[x]+=size[v]; 
			if(size[v]>size[son[x]])son[x]=v; //求出重兒子
		}	 	
	} 
}

DFS序


(以下內容引至洛谷講義)

我們DFS一棵樹的時候,對這棵樹的每個點按照訪問的時間進行重標號,就得到了樹的DFS序列。 這個序列可以有效地處理一些樹上的問題。 如圖就是一棵樹的一種可能的DFS序

記錄DFS的時候每個點訪問起始時間與結束時間,記最起始時間是前序遍歷,結束時間是后序遍歷。可以發現樹的一棵子樹在DFS序上是連續的一段

如圖, DFS之后,那么樹的每個節點就具有了區間的性質。那么此時,每個節點對應了一個區間,而且可以看到,每個節點對應的區間正好“管轄”了它子樹所有節點的區間,那么對點或子樹的操作就轉化為了對區間的操作。

蒟蒻的一句話總結

	以一個節點為起點,不往回走,一直搜到它能到達的所有點,優先走重兒子,輕兒子等重兒子走完以后回溯上來再往下走。
	而每個點的dfs序就是它按順序被搜到的時間點(前面有多少個點已經被搜過了)。
void dfs2(int x,int t)
{
	//l[]表示這個點的dfs序
	//然后a[]是以dfs序為下標(維護鏈)的當前點的點值
	//ch[]是題目給出的點值
	//top[]記錄的是這個點所在的鏈的頂端的那個點,用來跳lca
	l[x]=++tot;a[tot]=ch[x];top[x]=t; 
	if(son[x])dfs2(son[x],t); //有重兒子維護重鏈
	for(int i=head[x];i;i=e[i].next) 
	{
		int v=e[i].to; 
		if(v!=fa[x]&&v!=son[x]) 
		//不是父親不是重兒子,以輕兒子為端點的新鏈dfs下去
		dfs2(v,v); 
	} 
}

LCA


當我們把當前的樹剖成了鏈以后,我們要怎么做呢?
一個一個訪問顯然是不可能的,有倍增法跳lca的例子在前,我們是不是也可以模仿呢?

首先我們要搞清楚鏈是怎么下放到線段樹上去的。
以某個點為例,先把這個點的一條重鏈下放,在回溯過程中搜索以其他輕兒子為頂端的重鏈下放。所以在線段樹中存的點的順序便是節點的dfs序值。

當我們把一顆樹剖成鏈以后我們可以注意到如下特點

	1.如果要詢問以x的子樹信息,在之前求輕重鏈的時候我們已經維護好了x的size大小,然后根據dfs序的遍歷特點,我們可以得出如果是關於子樹的操作,就是在線段樹上詢問 l[表示dfs序的數組] + size[當前節點]-1 ,-1是因為它本身重復算了兩次。這樣看來子樹在線段樹上是一段連續的區間,這樣是不需要跳lca的。
	2.我們發現x到y上的路徑可能會經過幾條重鏈和輕鏈,而它們並不一定在線段樹上是連續的一段,那么這樣的話我們就需要依次維護這幾條鏈了,也就是說我們用可以跳鏈的方法一邊進行維護一邊跳lca。

那么我們要怎么進行跳鏈維護呢?
這個算法很明顯的是用來節約時間的算法。
是不是我們就可以這么想:

	既然以x的子樹信息可以直接調用,又x到這條重鏈上的任一一點在線段樹上都是一段連續的區間。那么x到y的路徑就是通過x和y不斷的從自身的鏈往上跳,一次跳一條重鏈因為重鏈在線段樹上可以直接維護。然后就是要清楚當前x和y誰的top節點深度更深,因為跳深度低的可能會錯過lca。那么跳鏈的方法就出來了。

讓我們再以這張圖為例

我們現在要讓10和5跳lca。很明顯我們要從10跳到4,因為5會直接跳到1而它們的lca很明顯是2。
下面我們貼出一個給x到y路徑加值的代碼

void cal2(int x,int y,int v) 
{
	int fx=top[x],fy=top[y]; 
	while(fx!=fy)//如果頂點相同了就不用跳了直接在線段樹上下放 
	{ 
		//按上面說的判斷top深度
		if(dep[fx]<dep[fy])swap(x,y),swap(fx,fy); 
		update(1,1,tot,l[fx],l[x],v); 
		x=fa[fx],fx=top[x];
		//x換到另一條鏈繼續往上跳 
	} 
	if(l[x]>l[y])swap(x,y); 
	//在這里dfs序和dep之間是沒有區別的
	//因為深度深的點在同一條重鏈里很明顯是后被dfs到的
	update(1,1,tot,l[x],l[y],v); 
}

模板


這樣的話我們就說完了樹鏈剖分的基本思路
下面貼出洛谷的模板
P3384 【模板】樹鏈剖分
題意
給定一顆樹維護它的子樹和路徑加值問題。可以說是非常模板了。

#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<iostream>
#define ll long long 
using namespace std;
struct node{
    ll to,next;
}e[500001];
ll rt,mod;
ll head[500001],dep[500001],sum[500001],a[500001];
ll tot,num,n,m,lazy[500001],fa[500001],l[500001];
ll ch[500001],top[500001],size[500001],son[500001];
void build(int root,int l,int r)
{
    if(l==r){sum[root]=a[l];return ;}
    int mid=(l+r)>>1;
    build(root<<1,l,mid);
    build(root<<1|1,mid+1,r);
    sum[root]=sum[root<<1]+sum[root<<1|1];sum[root]%=mod;
    return ;
}

void push(int root,int l,int r)
{
    int mid=(l+r)>>1;
    lazy[root<<1]+=lazy[root];lazy[root<<1]%=mod;
    lazy[root<<1|1]+=lazy[root];lazy[root<<1|1]%=mod;
    sum[root<<1]+=lazy[root]*(mid-l+1);sum[root<<1]%=mod;
    sum[root<<1|1]+=lazy[root]*(r-mid);sum[root<<1|1]%=mod;
    lazy[root]=0;
    return ;
}

void update(int root,int left,int right,int l,int r,ll k)
{
    if(l<=left&&r>=right)
    {
    sum[root]+=k*(right-left+1);sum[root]%=mod;
    lazy[root]+=k;lazy[root]%=mod;
    return;
    }
    if(left>r||right<l)return ;
    int mid=(left+right)>>1;
    if(lazy[root])push(root,left,right);
    if(mid>=l)update(root<<1,left,mid,l,r,k);
    if(mid<r) update(root<<1|1,mid+1,right,l,r,k);
    sum[root]=(sum[root<<1|1]+sum[root<<1])%mod;
    return;
}

ll query(int root,int left,int right,int l,int r)
{
    if(l<=left&&r>=right)return sum[root]%mod;
    if(left>r||right<l)return 0;
    int mid=(left+right)>>1;
    if(lazy[root])push(root,left,right);
    ll a=0,b=0;
    if(mid>=l) a=query(root<<1,left,mid,l,r);
    if(mid<r)  b=query(root<<1|1,mid+1,right,l,r);
    return (a%mod+b%mod)%mod;
}

--------------------以上是線段樹分割線-------------------------

void dfs1(int x)
{
    size[x]=1;
    for(int i=head[x];i;i=e[i].next)
    {
        int v=e[i].to;
        if(!dep[v])
        {
            dep[v]=dep[x]+1;
            fa[v]=x;
            dfs1(v);
            size[x]+=size[v];
            if(size[v]>size[son[x]])son[x]=v;
        }
    }
}

void dfs2(int x,int t)
{
    l[x]=++tot;a[tot]=ch[x];top[x]=t;
    if(son[x])dfs2(son[x],t);
    for(int i=head[x];i;i=e[i].next)
    {
        int v=e[i].to;
        if(v!=fa[x]&&v!=son[x])
        dfs2(v,v);
    }
    return ;
}

ll cal1(int x,int y)//查詢路徑值
{
    ll maxx=0;
    int fx=top[x],fy=top[y];
    while(fx!=fy)
    {
        if(dep[fx]<dep[fy])swap(x,y),swap(fx,fy);
        maxx+=query(1,1,tot,l[fx],l[x]);
        x=fa[fx];fx=top[x];
    }
    if(l[x]>l[y])swap(x,y);
    maxx+=query(1,1,tot,l[x],l[y]);
    return maxx;
}

void cal2(int x,int y,int v)//維護路徑加值
{
    int fx=top[x],fy=top[y];
    while(fx!=fy)
    {
        if(dep[fx]<dep[fy])swap(x,y),swap(fx,fy);
        update(1,1,tot,l[fx],l[x],v);
        x=fa[fx],fx=top[x];
    }
    if(l[x]>l[y])swap(x,y);
    update(1,1,tot,l[x],l[y],v);
}

ll read()
{
    ll x=0,w=1;char ch=getchar();
    while(ch>'9'||ch<'0'){if(ch=='-')w=-1;ch=getchar();}
    while(ch>='0'&&ch<='9')x=x*10+ch-'0',ch=getchar();
    return x*w;
}

void add(int from,int to)
{
    num++;
    e[num].to=to;
    e[num].next=head[from];
    head[from]=num;
}
int main()
{
    n=read();m=read();rt=read();mod=read();
    for(int i=1;i<=n;i++)ch[i]=read(),ch[i]%=mod;
    for(int i=1;i<n;i++)
    {
        int x=read(),y=read();
        add(x,y);add(y,x);
    }
    dep[rt]=1;fa[rt]=1;
    dfs1(rt);dfs2(rt,rt);build(1,1,n);
    while(m--)
    {
        int qwq=read();
        if(qwq==1){int x=read(),y=read(),z=read();cal2(x,y,z%mod);}
        if(qwq==2){int x=read(),y=read();printf("%lld\n",cal1(x,y)%mod);}
        if(qwq==3){int x=read(),y=read();update(1,1,n,l[x],l[x]+size[x]-1,y%mod);}
        if(qwq==4){int x=read();printf("%lld\n",query(1,1,n,l[x],l[x]+size[x]-1)%mod);}
    //子樹可以直接調用
    }
    return 0;
}

好了到此模板就教完了,我們來講一點關於樹鏈剖分有意思的題目。
順便提一句
考樹鏈剖分不是考剖分而是考線段樹!!!!


關於樹鏈剖分基本操作的模板題


【 題解 】 P3178 [HAOI2015]樹上操作
【 題解 】P2590 [ZJOI2008]樹的統計
【 題解 】P2146 [NOI2015]軟件包管理器]

這里的每一道題都是維護子樹和路徑的,甚至不需要什么思路,只要會一點點樹鏈剖分的板子就可以了233333333,打完就入門了。


邊轉點問題


有時候我們會看到這樣一些題目,它們不會給定每個點的值,而是給定每條路徑的值,這樣我們又要如何維護呢?

首先思考一波拆邊,像lct那樣的,棄療了
后來某YZK大佬安利我一句,從下往上跳lca又兒子到父親只有一條邊,是不是就可以把邊權值給那個點的兒子節點呢,因為給一個父親的話,父親是不是會有幾個兒子,值就會混亂,但是兒子只有一個父親,就o**k了。

在這里狂膜%%%%%YZK大佬

果然吊打我幾萬里。

所以做這類問題的思路便出來了,我們考慮把每一條邊的值賦給它的兒子節點,注意這樣在跳lca的時候,lca的值是要減去的,因為它的值是它到它父親的邊權,不包括在這次跳鏈的過程中。
建議不懂的畫個圖理解一下emmm。
就不給圖了略略略略

給一道比較好做的模板題
【 題解 】P3038 [USACO11DEC]牧草種植Grass Planting


多重建樹問題

做過永無鄉的julao們應該都知道,在維護每一個聯通塊的時候,我們都要以一個節點單獨建splay。然后lct中維護顏色聯通塊時,也有類似思想。

那么是不是能把這個思想運用到樹剖上呢?

讓我們來分析一下下列問題。

一條路徑上,每次詢問的是同一條路徑,但是這條路徑維護了幾個不同的信息。

這里的不同的信息指的是,比如:這一條路徑有幾個不同的屬性,我每次能查詢或修改隨機的一個屬性,這時候我們就要保存每個屬性來提供答案。

那么當我們觀察到這個有關屬性是一個小范圍的值的時候,我們就可以考慮一手維護多個線段樹,注意一般不是建多條鏈,而是把根據一條鏈的幾個屬性分別建樹,然后按照對應的屬性在那個線段樹上查詢。
一般思想不難,就是難調咕咕咕

經典例題:

BJOI2018求和
它要維護一個數的k次方,而k<=50
這樣我們就可以思考把每一個次方放到一顆對應的線段樹去維護。
調了我兩天qaq(我果然是太菜了)
【 題解 】P4427 [BJOI2018]求和


博主(caiji)自己yy的操作

有時候學東西學多了,就會開始不經意的思考 (作死)
至於怎么作死呢,當時博主在學分塊emmm,是的,自己在yy樹上分塊

畢竟同樣是把樹鏈下放,下放到什么樣的數據結構不隨便我們自己嗎。。。那么我們豈不是就可以開始操作(作死)了

比如像這樣一個題目(YYJ的原創題)

題目描述:

Brave_cattle學姐,
當我來到這個世界的時候,我不知道我曾會遇見你。
在這個世界即將要毀滅的邊緣,
我們又還能在一起做些什么? 還能在躺在草地上嗎?
想着曾經的點點星空,原來,時間過得是這么的快啊。
在生命終結的時候,
讓我們再看着星空死去吧。
嗯? 為什么此時此刻的星空有點奇怪?
宛如星座一般的邊把星星怎么連成了一棵樹?
怎么,那不是星星!是即將砸過來的隕石!
某個時刻,x 到 y 的路徑上的星星靠近度又增加了 z。
某個時刻,我們需要知道 x 到 y 的路徑上的星星的靠近度大於等於 z 的數量。
刻不容緩。 末日就要到了。
輸入
第一行 n 個點,m 個時刻 第二行 給定 1—n 個點的靠近度
接下來 n-1 行 表示 x 和 y 有一條連邊 接下來 m 行表示 m 個時刻 每個時刻有兩種情況
Q x y z 表示詢問 x 到 y 的路徑上的星星的靠近度大於等於 z 的數
M x y z 表示 x 到 y 的路徑上的星星靠近度又增加了 z
輸出
需要對每個 Q 的時刻進行回答

樣例 輸入

10 5
1 2 3 4 5 6 7 8 9 10
1 2
1 3
3 5
3 6
2 4
4 7
4 8
8 10
8 9
Q 1 10 4
M 1 9 3
M 1 10 1
Q 1 7 9
Q 1 7 8

輸出

3
0
1

數據范圍
第 1,2 個點
1<=n,m<=500 1<=靠近度值<=10^4
第 3,4,5,6,7,8,9,10 個點
1<=n<=1000000,1<=m<=5000,1<=靠近度值<=10^6

ps:數據保證靠近度值不超過 int

完整的題目背景戳這里

博客沒事干搞出來的 浪費時間沒有難度的題目難題

后面因為樹的中心靠上且沒有生成較長的鏈,別人輕松模擬比正解快三倍,身敗名裂

所以這個題目要怎么寫呢,
權值線段樹+樹鏈剖分????貌似可以這么寫
反正我寫的是分塊加樹鏈剖分
原題是教主的魔法,我只是把它壓到樹上了
所以就是先按教主的魔法打好,然后在按樹剖后的dfs序下放進去,julao們告訴我這太麻煩了????反正我太菜了,其他騷操作又沒學

就是在你沒學習歐拉序的情況下,
麻煩一點強行樹上分塊,時間復雜度在分塊的基礎上再加上一個log

好了,樹鏈剖分加分塊已經有了:
主席樹加樹鏈剖分還會遠嗎?
單調隊列加樹鏈剖分還會遠嗎?
莫隊加樹鏈剖分還會遠嗎?(雖然有樹上莫隊來着


更新未完待續,其實是博主generals去了咕咕咕咕


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM