树链剖分

树链剖分

基础概念
重儿子:父节点的所有儿子中子树节点数目最多的节点
轻儿子:父节点除重儿子以外的节点
重边:父节点和重儿子连成的边
轻边:父节点和轻儿子连成的边
重链:由多条重边连接而成的路径
如下图,绿色边即为重边
image

性质
1.整棵树会被剖分成若干条重链(可以将单独的叶子节点看作一条重链)
2.轻儿子一定是每条重链的顶点
3.任意一条路径被切分成不超过 \(\log n\) 条链

用到的 5 个数组:

  • fa[u]:存节点 u 的父节点

  • dep[u]:存节点 u 的深度

  • son[u]:存节点 u 的重儿子

  • sz[u]:存以节点 u 为根的子树的节点个数

  • top[u]:存 u 所在的重链的顶点,即链头

利用两个 dfs 处理这五个数组(4 + 1)
(下面是用邻接表实现的)

dfs1 :预处理数组 dep、sz、fa、son,初始为 dfs1 (根节点, 0)
从根开始遍历每个节点,统计深度、父节点和节点数初值,最主要就是递归后判断重儿子的位置

int dep[maxm], fa[maxm], son[maxm], sz[maxm];//树剖基础
void dfs1(int u, int f){//预处理dep、sz、fa、son
	dep[u] = dep[f] + 1;
	sz[u] = 1;
	fa[u] = f;
	for(auto v : e[u]){
		if(v == f) continue;
		dfs1(v, u);
		sz[u] += sz[v];
		if(sz[v] > sz[son[u]]) son[u] = v;
	}
	return ;
}

dfs2 :预处理数组 top,初始为 dfs2 (根节点,根节点)
首先赋值链头,如果没有重儿子直接返回,再 dfs 重儿子(重儿子的链头是当前的链头),最后遍历轻儿子,但是轻儿子的链头是自己

int top[maxm];
void dfs2(int u, int t){//预处理top
	top[u] = t;
	if(!son[u]) return ;
	dfs2(son[u], t);
	for(auto v : e[u]){
		if(v == fa[u] || v == son[u]) continue;
		dfs2(v, v);
	}
	return ;
}

重链剖分

利用重链剖分将树划分为若干条重链,再将其投射到数组上,利用线段树维护区间信息,实现子树的修改

注意点:
1.对于树上点权的修改一定要转化为对于生成区间的修改,即必须利用 id 函数转换下标!!!

参考板子

const int N = 2e5 + 5;//节点数
vector<int> e[N];//邻接表
int val[N];//初始权值

int dep[N], fa[N], son[N], sz[N];//树剖基础
void dfs1(int u, int father){//预处理dep、sz、fa、son
	dep[u] = dep[father] + 1;
	sz[u] = 1;
	fa[u] = father;
	for(auto v : e[u]){
		if(v == father) continue;
		dfs1(v, u);
		sz[u] += sz[v];
		if(sz[v] > sz[son[u]]) son[u] = v;
	}
	return ;
}

int top[N], id[N], nw[N], cnt = 0;//重链增加
void dfs2(int u, int t){
	top[u] = t;
	id[u] = ++ cnt;		//id 赋值,cnt 从 0 开始,但 id 从 1 开始
	nw[cnt] = val[u];	//nw 赋值
	if(son[u] == 0) return ;
	dfs2(son[u], t);
	for(auto v : e[u]){
		if(v == fa[u] || v == son[u]) continue;
		dfs2(v, v);
	}
	return ;
}

下面以第一个例题 - 洛谷 P3384 【模板】重链剖分/树链剖分 为例,说说如何实现子树的节点权值的修改和节点间最短路径上的节点权值的修改和查询
在上面的 dfs1 和 dfs2 预处理的基础上,我们再增加两个数组:id 和 nw

  • id[u]:标识节点 u 的剖分后的新编号
  • nw[u]:存新编号在树中所对应节点的权值

在 dfs2 中,我们实现对 id 和 nw 的初始化。代码如下:

int top[maxm], id[maxm], nw[maxm], cnt = 0;
void dfs2(int u, int t){
	top[u] = t;
	id[u] = ++ cnt;		//id 赋值,cnt 从 0 开始,但 id 从 1 开始
	nw[cnt] = seg[u];	//nw 赋值
	if(son[u] == 0) return ;
	dfs2(son[u], t);
	for(auto v : e[u]){
		if(v == fa[u] || v == son[u]) continue;
		dfs2(v, v);
	}
	return ;
}

在这样处理了之后,我们就已经将树按照一定的规律投射到数组上,并且记录其下标了。
再对生成数组建立线段树,即可得到维护原树节点权值的线段树

根据树链剖分的原理,我们可以便利地对子树权值进行整体查询和修改以及两节点之间最短路径上的节点的权值进行修改和查询

对子树的整体修改和查询
以 u 为根的子树投射到生成数组的下标范围为 $[id[u], id[u] + sz[u] - 1] $
所以对子树的修改和查询即为对该区间的修改和查询

对最短路径上的节点权值的修改和查询
对于树上节点 u 和节点 v 之间的最短路径进行操作。
类似于 LCA 求最近公共祖先的原理,当两个节点不在一条重链上时,假设节点 u 的深度更大,那么在节点 u 跳到其链头的父节点之前,对其到链头这段最短路径的一部分进行维护之后再跳;当跳到一条重链上之后,在对两节点之间最后的最短路径进行维护即可
注意维护时,映射下标的大小不要弄反

下为代码

void update_path(int u, int v, ll k){
	while(top[u] != top[v]){
		if(dep[top[u]] < dep[top[v]]) swap(u, v);
		update(1, 1, n, id[top[u]], id[u], k);
		u = fa[top[u]];
	}
	if(dep[u] < dep[v]) swap(u, v);
	update(1, 1, n, id[v], id[u], k);
	return ;
}

ll query_path(int u, int v){
	ll res = 0;
	while(top[u] != top[v]){
		if(dep[top[u]] < dep[top[v]]) swap(u, v);
		res = (res + query(1, 1, n, id[top[u]], id[u])) % mod;
		u = fa[top[u]];
	}
	if(dep[u] < dep[v]) swap(u, v);
	res = (res + query(1, 1, n, id[v], id[u])) % mod;
	return res;
}

例题

洛谷 P3384 【模板】重链剖分/树链剖分

代码

//>>>Qiansui
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define mem(x,y) memset(x, y, sizeof(x))
#define debug(x) cout << #x << " = " << x << '\n'
#define debug2(x,y) cout << #x << " = " << x << " " << #y << " = "<< y << '\n'
//#define int long long

using namespace std;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef pair<ull, ull> pull;
typedef pair<double, double> pdd;
/*

*/
const int maxm = 1e5 + 5, inf = 0x3f3f3f3f;
int n, m, r, mod, cnt = 0;
ll  seg[maxm << 2], tag[maxm << 2];
int dep[maxm], fa[maxm], son[maxm], sz[maxm], top[maxm];//树剖基础
int id[maxm], nw[maxm]; //重链剖分增加
vector<int> e[maxm];

void dfs1(int u, int father){
	dep[u] = dep[father] + 1;
	fa[u] = father;
	sz[u] = 1;
	for(auto v : e[u]){
		if(v == father) continue;
		dfs1(v, u);
		sz[u] += sz[v];;
		if(sz[v] > sz[son[u]]) son[u] = v;
	}
	return ;
}

void dfs2(int u, int t){
	top[u] = t;
	id[u] = ++ cnt;
	nw[cnt] = seg[u];
	if(son[u] == 0) return ;
	dfs2(son[u], t);
	for(auto v : e[u]){
		if(v == fa[u] || v == son[u]) continue;
		dfs2(v, v);
	}
	return ;
}

int ls(int p) { return p << 1; }
int rs(int p) { return p << 1 | 1; }

void push_up(int p) { seg[p] = (seg[ls(p)] + seg[rs(p)]) % mod; return ;}

void build(int p, int pl, int pr){
	tag[p] = 0;
	if(pl == pr){
		seg[p] = nw[pl] % mod;
		return ;
	}
	int mid = pl + pr >> 1;
	build(ls(p), pl, mid);
	build(rs(p), mid + 1, pr);
	push_up(p);
	return ;
}

void addtag(int p, int pl, int pr, ll k){
	tag[p] = (tag[p] + k) % mod;
	seg[p] = (seg[p] + (pr - pl + 1) * k % mod) % mod;
	return ;
}

void push_down(int p, int pl, int pr){
	if(tag[p]){
		int mid = pl + pr >> 1;
		addtag(ls(p), pl, mid, tag[p]);
		addtag(rs(p), mid + 1, pr, tag[p]);
		tag[p] = 0;
	}
	return ;
}

void update(int p, int pl, int pr, int l, int r, ll k){
	if(l <= pl && pr <= r){
		addtag(p, pl, pr, k); return ;
	}
	push_down(p, pl, pr);
	int mid = pl + pr >> 1;
	if(l <= mid) update(ls(p), pl, mid, l, r, k);
	if(mid < r) update(rs(p), mid + 1, pr, l, r, k);
	push_up(p);
	return ;
}

ll query(int p, int pl, int pr, int l, int r){
	if(l <= pl && pr <= r) return seg[p];
	push_down(p, pl, pr);
	int mid = pl + pr >> 1;
	ll res = 0;
	if(l <= mid) res = (res + query(ls(p), pl, mid, l, r)) % mod;
	if(mid < r) res = (res + query(rs(p), mid + 1, pr, l, r)) % mod;
	return res;
}

void update_path(int u, int v, ll k){
	while(top[u] != top[v]){
		if(dep[top[u]] < dep[top[v]]) swap(u, v);
		update(1, 1, n, id[top[u]], id[u], k);
		u = fa[top[u]];
	}
	if(dep[u] < dep[v]) swap(u, v);
	update(1, 1, n, id[v], id[u], k);
	return ;
}

ll query_path(int u, int v){
	ll res = 0;
	while(top[u] != top[v]){
		if(dep[top[u]] < dep[top[v]]) swap(u, v);
		res = (res + query(1, 1, n, id[top[u]], id[u])) % mod;
		u = fa[top[u]];
	}
	if(dep[u] < dep[v]) swap(u, v);
	res = (res + query(1, 1, n, id[v], id[u])) % mod;
	return res;
}

void solve(){
	cin >> n >> m >> r >> mod;
	//建树,重链剖分抽象成线段树
	for(int i = 1; i <= n; ++ i) cin >> seg[i];
	for(int i = 1; i < n; ++ i){
		int u, v;
		cin >> u >> v;
		e[u].push_back(v);
		e[v].push_back(u);
	}
	dfs1(r, 0);
	dfs2(r, r);
	build(1, 1, n);
	//修改与查询
	for(int i = 0; i < m; ++ i){
		int c, x, y, z;
		cin >> c;
		if(c == 1){
			cin >> x >> y >> z;
			update_path(x, y, z);
		}else if(c == 2){
			cin >> x >> y;
			cout << query_path(x, y) << '\n';
		}else if(c == 3){
			cin >> x >> z;
			update(1, 1, n, id[x], id[x] + sz[x] - 1, z);
		}else{
			cin >> x;
			cout << query(1, 1, n, id[x], id[x] + sz[x] - 1) << '\n';
		}
	}
	return ;
}

signed main(){
	ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
	int _ = 1;
	// cin >> _;
	while(_ --){
		solve();
	}
	return 0;
}

#10138. 「一本通 4.5 例 1」树的统计

简单的两点间最短路径维护点权和修改单点权
做的时候一直 wa,原因是单点修改的时候,修改的下标应该是 id[u],而不是 u,因为 u 是树上的序号,而不是生成数组的下标!!!
Qiansui_code


洛谷 P2486 [SDOI2011] 染色

这题利用重链剖分 + 线段树的知识即可解决
不是普通的线段树,是子区间合并需考虑相互影响的线段树
后面的这种线段树可见2023暑假训练 - 线段树 - H
代码:Qiansui_code
关键代码

void push_up(int p){
	int pl = ls(p), pr = rs(p);
	seg[p].ans = seg[pl].ans + seg[pr].ans;
	seg[p].lc = seg[pl].lc;
	seg[p].rc = seg[pr].rc;
	if(seg[pl].rc == seg[pr].lc) -- seg[p].ans;
	return ;
}

node query(int p, int pl, int pr, int l, int r){
	if(l <= pl && pr <= r) return seg[p];
	push_down(p);
	int mid = pl + pr >> 1;
	if(r <= mid) return query(ls(p), pl, mid, l, r);
	else if(mid < l) return query(rs(p), mid + 1, pr, l, r);
	else{
		node x, y, z;
		x = query(ls(p), pl, mid, l, mid);
		y = query(rs(p), mid + 1, pr, mid + 1, r);
		z.ans = x.ans + y.ans;
		z.lc = x.lc;
		z.rc = y.rc;
		if(x.rc == y.lc) -- z.ans;
		return z;
	}
}

例题集合


长链剖分


相关资料

dx123 - 树链剖分法
dx123 - 重链剖分
oiwiki - 树链剖分


例题

求最近公共祖先

posted on 2023-08-08 22:50  Qiansui  阅读(9)  评论(0编辑  收藏  举报