专题·树链剖分【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——

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