樹鏈剖分入門詳解+入門題推薦


以前沒有接觸過樹鏈剖分的同學們看到這個東西是不是覺得很高大上呢,下面我將帶你們進入樹的世界(講得不好別打我

首先我們來看一道題

軟件包管理器

這道題的大意是,每個軟件有一個父軟件(除根節點外)。要安裝一個軟件必須先安裝它的父軟件,要卸載一個軟件必須先卸載它的所有子軟件,模擬對軟件的安裝和卸載操作

通過以上的分析,我們可以看出這大概是一個樹形結構。

由於節點個數和操作次數的范圍為\(1e5\),明顯要用一個\(nlgn\)或者\(nlg^2n\)的算法,那么就可以引出我們今天的主角——樹剖了。


樹鏈剖分

顧名思義,樹鏈剖分就是指將一顆樹分成若干條鏈,使得可以使用數據結構(例如線段樹,主席樹)來進行維護

它的特點很明顯,我們可以非常便捷的處理同一條鏈上的若干點和邊

直說上面大概會讓大家一頭霧水,這只是讓大家對樹剖有一個初步的概念,下面我們要開始正式的講解了

先下若干定義

重兒子:子樹最大的兒子

輕兒子:除了重兒子以外的兒子

重邊:父節點與重兒子組成的邊

輕邊:除重邊以外的邊

重鏈:重邊連接而成的鏈

輕鏈:輕邊連接而成的鏈

鏈頭:一條鏈上深度最小的點

下圖為例

圖中的紅色邊即為重邊,重邊連成的鏈,與紅色邊相連的所有節點,即為重兒子,0節點即為鏈頭

重鏈未必只有一條,每個子樹都至少有一條重鏈,如下圖

我們默認已經將鏈分好了,我們來說一下樹剖的相關操作

一般,我們需要用樹剖來解決以下問題

修改點x到點y路徑上各點的值

查詢點x到點y路徑上各點的值

修改點x子樹上各點的值

查詢點x子樹上各點的值

首先我們要對每個節點進行編號,保證在鏈上的節點編號是連續的,這樣子樹的編號也是連續的

那么我們每次對\(x,y\)兩個點進行詢問

若兩點在同一條鏈上,則直接在線段樹上進行詢問

若不在一條鏈上,則每次選擇鏈頭深度更大的一個往上跳,直到兩點在同一條鏈上

那么對於子樹的操作我們怎么搞呢?

很簡單,我們已經說了,子樹的編號是連續的,根節點的編號到加上子樹大小-1的區間即為子樹區間

大概是這個樣子,我們再回過頭來看開頭的那道題

我們直接放代碼吧

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cctype>
#define ll long long
#define gc() getchar()
#define maxn 100005
using namespace std;

inline ll read(){
	ll a=0;int f=0;char p=gc();
	while(!isdigit(p)){f|=p=='-';p=gc();}
	while(isdigit(p)){a=(a<<3)+(a<<1)+(p^48);p=gc();}
	return f?-a:a;
}
void write(ll a){
	if(a>9)write(a/10);
	putchar(a%10+'0');
}
int n,m;

struct ahaha1{      //首先,我們把依賴與被依賴的關系轉化為邊存起來
	int to,next;
}e[maxn];int tot,head[maxn];
inline void add(int u,int v){
	e[tot].to=v;e[tot].next=head[u];head[u]=tot++;
}

int f[maxn],sz[maxn],dep[maxn],son[maxn];     //f數組存儲當前節點的父節點,sz數組表示以當前節點為根的子樹大小,dep數組表示節點深度,son數組表示節點的重兒子
void dfs(int u,int fa){
	int maxa=0;sz[u]=1;      //sz初始為1,也就是僅包含自身
	for(int i=head[u];~i;i=e[i].next){    //遍歷所有子節點
		int v=e[i].to;if(v==fa)continue;
		f[v]=u;dep[v]=dep[u]+1;      
		dfs(v,u);sz[u]+=sz[v];
		if(sz[v]>maxa)maxa=sz[v],son[u]=v;     //不斷修改重兒子,保證為子樹最大的子節點
	}
}
int b[maxn],in[maxn],top[maxn];       //b數組存儲當前編號對應的節點,in數組表示當前節點的編號,top數組表示當前節點所處鏈的鏈頭
void dfs(int u,int fa,int topf){
	b[++tot]=u;in[u]=tot;top[u]=topf;
	if(!son[u])return;
	dfs(son[u],u,topf);     //與重兒子相連的邊為重邊,所以鏈頭不變
	for(int i=head[u];~i;i=e[i].next){
		int v=e[i].to;if(v==fa||v==son[u])continue;
		dfs(v,u,v);     //與其他兒子所連的邊為輕邊,所以鏈頭變為輕兒子
	}
}

struct ahaha2{
	int v,lz;
	ahaha2(){lz=-1;}
}t[maxn<<2];
#define lc p<<1
#define rc p<<1|1
inline void pushup(int p){
	t[p].v=t[lc].v+t[rc].v;
}
inline void pushdown(int p,int l,int r){
	int m=l+r>>1;
	t[lc].v=t[p].lz?m-l+1:0;t[lc].lz=t[p].lz;
	t[rc].v=t[p].lz?r-m:0;t[rc].lz=t[p].lz;
	t[p].lz=-1;
}
int update(int p,int l,int r,int L,int R,int z){      //合並了修改和查詢操作
	if(l>R||r<L)return 0;
	if(L<=l&&r<=R){int q=t[p].v;t[p].v=z?r-l+1:0;t[p].lz=z;return q;}
	int m=l+r>>1;if(t[p].lz!=-1)pushdown(p,l,r);
	int l1=update(lc,l,m,L,R,z),r1=update(rc,m+1,r,L,R,z);
	pushup(p);
	return l1+r1;
}

inline void solve_1(){     //需要放的物品數等於相關物品數減去已經放的物品數
	int x=read(),ans=0;
	while(top[x]){
		ans+=in[x]-in[top[x]]+1-update(1,1,n,in[top[x]],in[x],1);
		x=f[top[x]];
	}
	ans+=in[x]-in[0]+1-update(1,1,n,in[0],in[x],1);
	write(ans);
	putchar('\n');
}
inline void solve_2(){    //需要拿走的物品數為子樹上已安裝的物品數
	int x=read();
	write(update(1,1,n,in[x],in[x]+sz[x]-1,0));
	putchar('\n');
}

int main(){memset(head,-1,sizeof head);
	n=read();
	for(int i=1;i<n;++i){
		int u=read();
		add(u,i);
	}
	tot=0;dfs(0,-1);dfs(0,-1,0);
	m=read();string zz;
	for(int i=1;i<=m;++i){
		cin>>zz;
		switch(zz[0]){
			case 'i':solve_1();break;
			case 'u':solve_2();break;
		}
	}
	return 0;
}

是不是很簡單呢?想不想自己嘗試一下其他題目?

下面推薦幾道樹剖入門題

[SHOI2012]魔法樹

[ZJOI2008]樹的統計

月下“毛景樹”

[HAOI2015]樹上操作

aaa被續

Qtree3

[HEOI2016/TJOI2016]樹

[SDOI2011]染色

當然,還有我們的模板題【模板】樹鏈剖分

感謝您的閱覽,不妨點個贊再走啊


免責聲明!

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



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