專題·樹鏈剖分【including 洛谷·【模板】樹鏈剖分

初見安~~~終於學會了樹剖~~~

【興奮】當初機房的大佬在學樹剖的時候我反覆強調過:“學樹剖沒有前途的!!!”

恩。真香。

一、重鏈與重兒子

所謂樹剖——樹鏈剖分,就是賦予一個鏈的概念來優化一些或者說是應對一些操作的,所以相應的會有一些專用的概念定義。

 

一個節點的重兒子,爲其更大的一顆子樹的根節點。從這個點連向重兒子的邊我們稱爲重邊。

這裏我們定義子樹的大小是取決於節點的數量,而和點權沒有任何關係。

就比如在下圖中:

紅色的邊就是重邊。

我們一眼就可以發現——這些重邊多多少少連成了一條鏈。而這些由重邊連續連起來的點和邊就組成了重鏈,也就是樹鏈。

我們可以發現樹鏈的一些性質——比如,一條樹鏈一定是一個輕兒子或者根節點開頭,通過重邊串起來一些重兒子;一個非葉子節點只有一個重兒子;一個單獨的葉子節點也是一條樹鏈,【滿足以輕兒子開頭】,所以一棵樹一定可以被劃分爲幾條鏈等等。

那麼樹剖有什麼用呢——處理樹上的一些相關問題。比如——維護樹上區間,樹上路徑等等。

區間我們想到了線段樹,樹上路徑想到了LCA【傳送門建設中】,但是它們都有一個特點——連續。線段樹只能維護連續區間,LCA路徑也是不間斷的。所以爲了便於處理,我們要對這個圖重新標號,以便查找。怎麼標呢?我們可以想到——在樹鏈上操作LCA路徑,那麼路徑也是要連貫的,也就是說重鏈上的編號要連貫,所以我們重新編號的時候是在dfs序的基礎上遵循先遍歷重兒子的原則。

我們還是以一個模板題爲背景吧——洛谷P3384 【模板】樹鏈剖分

這個題有4個操作:

操作1: 格式: 1 x y z 表示將樹從x到y結點最短路徑上所有節點的值都加上z

操作2: 格式: 2 x y 表示求樹從x到y結點最短路徑上所有節點的值之和

操作3: 格式: 3 x z 表示將以x爲根節點的子樹內所有節點值都加上z

操作4: 格式: 4 x 表示求以x爲根節點的子樹內所有節點值之和

可以很容易發現——操作1、2都需要走一遍x到y的路徑,操作3、4都需要操作以x爲根的子樹。所以我們先思考怎麼遍歷這些區間——

首先遍歷x到y的路徑,我們亦容易想到LCA——兩個點同時往上跳,直到某個值相同,可以一起操作。所以我們的思路就是:兩個點不在同一條鏈就往鏈頭的父親節點跳,在同一條鏈上就直接處理。而處理方法也很簡單——因爲全程都在鏈上以連續的新節點編號來操作,所以線段樹維護區間距離就很方便了,完全不受樹剖影響地敲一個基本的建樹、查詢、區間修改+延遲標記的代碼就可以了。【不清楚沒關係,後面會有代碼和詳解】

而對於操作3和4,以x爲根的子樹,顯然編號也是連續的——畢竟編號時的最基本原則還是dfs遍歷。但是有一個小問題——我們知道以x爲根的子樹最小的編號是x的編號,但是最大的編號我們並不知道,如果遍歷一遍來找的話複雜度就會比較高了。所以——這是我們在初始化樹剖的時候要存儲下來的一個變量——以x爲根的子樹的最大的節點編號。也就是代碼中的cnt[ ]。

大致操作就如上啦~下面是代碼及詳解——

#include<bits/stdc++.h>
#define maxn 500010
using namespace std;
typedef long long ll;
struct graph//關於存圖的操作簡單打個包
{
	struct edge
	{
		ll to, nxt;
		edge(){}
		edge(ll tt, ll nn)
		{
			to = tt;
			nxt = nn;
		}
	}e[maxn << 1];
	ll head[maxn], k = 0;
	void init()
	{
		memset(head, -1, sizeof head);
	}
	
	void add(ll u, ll v)
	{
		e[k] = edge(v, head[u]);
		head[u] = k++;
	}
}g;

struct node
{
	ll c, f;//c是區間和,f是延遲標記
}t[maxn << 2];

ll fa[maxn], dep[maxn], size[maxn], son[maxn];
ll dfn[maxn], top[maxn], cnt[maxn], tot;
//cnt——該子樹最大節點編號(線段樹上)dfn映射原標號與新標號
ll n, m, root, val[maxn], mod;
//val爲節點上的值 
//*****************************************************
//關於初始化 
//to find the heavy son
void dfs_getson(ll u)//深優遍歷初始化並得出重兒子
{
	size[u] = 1;
	for(ll i = g.head[u]; ~i; i = g.e[i].nxt)
	{
		ll v = g.e[i].to;
		if(v == fa[u]) continue;
		fa[v] = u;
		dep[v] = dep[u] + 1;
		dfs_getson(v);
		size[u] += size[v];
		if(size[v] > size[son[u]]) son[u] = v;//這個兒子更大,這纔是重兒子
	}
}

void dfs_rewrite(ll u, ll tp)//找到鏈頭
{
	top[u] = tp;
	dfn[u] = ++tot;//映射到線段樹上 
	if(son[u]) dfs_rewrite(son[u], tp);//先遍歷重鏈
	for(ll i = g.head[u]; ~i; i = g.e[i].nxt)
	{
		ll v = g.e[i].to;
		if(v != son[u] && v != fa[u]) dfs_rewrite(v, v);其他輕兒子的鏈
	}
	cnt[u] = tot; //回溯標記該子樹最大編號
}

//*************************************************
//關於線段樹 【基本和線段樹模板1一模一樣
void build(ll p, ll l, ll r)
{
	if(l == r) 
	{
		t[p].c = val[id[l]];//線段樹上用的是新的編號
		return; 
	}
	
	ll mid = l + r >> 1;
	build(p << 1, l, mid);
	build(p << 1 | 1, mid + 1, r);
	t[p].c = t[p << 1].c + t[p << 1 | 1].c;
}

void down(ll p, ll l, ll r)
{
	t[p << 1].f += t[p].f;
	t[p << 1 | 1].f += t[p].f;
	ll mid = l + r >> 1;
	t[p << 1].c += t[p].f * (mid - l + 1);
	t[p << 1 | 1].c += t[p].f * (r - mid);
	t[p].f = 0;
}

void change(ll p, ll l, ll r, ll ls, ll rs, ll x)
{
	if(ls <= l && r <= rs)
	{
		t[p].c += (r - l + 1) * x;
		t[p].f += x;
		return;
	}
	if(t[p].f) down(p, l, r);
	ll mid = l + r >> 1;
	if(ls <= mid) change(p << 1, l, mid, ls, rs, x);
	if(rs > mid) change(p << 1 | 1, mid + 1, r, ls, rs, x);
	t[p].c = t[p << 1].c + t[p << 1 | 1].c;
}

ll getsum(ll p, ll l, ll r, ll ls, ll rs)
{
	if(ls <= l && r <= rs) 
	{
		
		return t[p].c;
	}
	if(t[p].f) down(p, l, r);
	ll mid = l + r >> 1;
	ll ans = 0;
	if(ls <= mid) ans += getsum(p << 1, l, mid, ls, rs), ans %= mod;
	if(rs > mid) ans += getsum(p << 1 | 1, mid + 1, r, ls, rs), ans %= mod;
	return ans;
}
//******************************************************
//關於操作入口
void change_xtoy()//1
{
	ll x, y, z;
	scanf("%lld%lld%lld", &x, &y, &z);
	while(top[x] != top[y])
	{
		if(dep[top[x]] > dep[top[y]]) swap(x, y);
		change(1, 1, tot, dfn[top[y]], dfn[y], z);範圍從y到y的鏈頭
		y = fa[top[y]];//深度更深的一個往上跳
	}
	if(dep[x] > dep[y]) swap(x, y);//on the same line
	change(1, 1, tot, dfn[x], dfn[y], z);
}

void getson_xtoy()//2
{
	ll x, y;
	scanf("%lld%lld", &x, &y);
	ll ans = 0;
	while(top[x] != top[y])
	{
		if(dep[top[x]] > dep[top[y]]) swap(x, y);
		ans = (ans + getsum(1, 1, tot, dfn[top[y]], dfn[y])) % mod;		
		y = fa[top[y]];
	}
	if(dep[x] > dep[y]) swap(x, y);
	ans += getsum(1, 1, tot, dfn[x], dfn[y]);
	prllf("%lld\n", ans % mod);
}

void change_sontree()//3
{
	ll x, y;
	scanf("%lld%lld", &x, &y);
	change(1, 1, tot, dfn[x], cnt[x], y);//直接操作
}

void getsum_sontree()//4
{
	ll x;
	scanf("%lld", &x);
	prllf("%lld\n", getsum(1, 1, tot, dfn[x], cnt[x]) % mod);
} 

ll main()
{
	g.init();
	scanf("%lld%lld%lld%lld", &n, &m, &root, &mod);
	for(ll i = 1; i <= n; i++) scanf("%lld", &val[i]);
	for(ll i = 1; i < n; i++)
	{
		ll u, v;
		scanf("%lld%lld", &u, &v);
		g.add(u, v);
		g.add(v, u);
	}
	
	dfs_getson(root);
	dfs_rewrite(root, root);//初始化
	
	build(1, 1, tot);
	
	for(ll i = 1; i <= m; i++)
	{
		ll op;
		scanf("%lld", &op);
		if(op == 1) change_xtoy();
		if(op == 2) getson_xtoy();
		if(op == 3) change_sontree();
		if(op == 4) getsum_sontree();
		
	}
	return 0;
}

也許這份代碼看起來很長,(有很多醜陋的註釋)但是核心的部分也就只有初始化的兩個dfs函數和操作1、2的入口函數xtoy,加起來50行左右,所以樹剖只要理解到了倒也挺簡單的:)而在往後的各大知識點的學習中(什麼LCT,DDP……反正我都不會)樹剖更是作爲了一個基礎知識,所以一定要掌握好哇~~

迎評:)
——End——

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章