树上启发式合并

这个东西可以解决一些静态,只关心子树贡献的问题。

CF600E

板子。先把重儿子找到。每次先把每个轻儿子的答案暴力算掉,暴力清空贡献。然后对重儿子算完之后,不清空其子树内的贡献往上走,暴力把其它轻儿子的贡献加入。时间复杂度 \(O(n\log n)\)

重链剖分

概念

一些概念:

  • 重儿子:对于一个非叶子节点,它所有儿子中儿子数量最多的那个儿子。
  • 轻儿子:对于一个非叶子节点,其余儿子即为轻儿子。
  • 重边:连接两个重儿子的边。
  • 轻边:剩下的边即为轻边。
  • 重链:一条全部为重儿子的路径,除了起点是轻儿子

然后我们通过两遍 dfs,第一次预处理出 \(dep_u,siz_u,son_u,fa_u\) 表示 \(u\) 的深度,子树大小,重儿子编号,父亲编号。
第二次处理出 \(id_u,top_u\) 表示 \(u\) 是第几个遍历到的,\(u\) 所在重链的顶点。

void dfs2(ll u, ll topu) {
	top[u] = topu; 
	id[u] = ++ tot; 
	if (son[u]) 
		dfs(son[u], topu); 
	if (!son[u]) return ; 
	for (auto v : G[u]) {
		if (v == fa[u] || v == son[u]) continue; 
		dfs2(v, v); 
	}
}

然后有两个很好的性质:

  • \(u\) 子树内编号 \(id\) 连续。
  • 任意重链编号连续。

这样就把一珂树划分成了几条链,把树反映为序列,使得很多处理线性结构的数据结构能在树上应用。
有一个结论是,可以证明在任意一个结点向根的路径上,重链和轻边条数,均不超过 \(\log n\)

应用

将树上 \(x\)\(y\) 路径进行某种操作

以区间加为例。与倍增求 lca 类似地,我们不断让 \(x,y\) 暴力往上跳重链,每次让 \(x\) 所在重链顶到 \(x\) 做区间加,\(y\) 同理,直到 \(x,y\) 跳到同一个重链上,使用数据结构维护。

void addPat(ll u, ll v, ll w) {
	while (top[u] != top[v]) {
		if (dep[top[u]] < dep[top[v]]) swap(u, v); 
		modif(id[top[u]], id[u], w); 
		u = fa[top[u]]; 
	}
	if (dep[u] > dep[v]) swap(u, v); 
	modif(id[u], id[v], w); 
}

询问 \(x\)\(y\) 路径上的信息

同理。

ll askPat(ll u, ll v) {
	ll res = 0; 
	while (top[u] != top[v]) {
		if (dep[top[u]] < dep[top[v]]) swap(u, v); 
		res += ask(id[top[u]], id[u]); 
		u = fa[top[u]]; 
	}
	if (dep[u] > dep[v]) swap(u, v); 
	res += ask(id[u], id[v]); 
	return res; 
}

\(u\) 子树内进行某种操作

由于子树内编号连续,是容易的。

void addSon(ll u, ll w) {
	modif(id[u], id[u] + siz[u] - 1, w); 
} 

询问 \(u\) 子树内的信息

同理。

ll askSon(ll u) {
	return ask(id[u], id[u] + siz[u] - 1); 
} 

查询 \(x\)\(y\) 的 lca

ll lca(ll u, ll v) {
	while (top[u] != top[v]) {
		if (dep[top[u]] < dep[top[v]]) swap(u, v); 
		u = fa[top[u]]; 
	}
	return dep[u] < dep[v] ? u : v; 
}

P3384

模板。时间复杂度 \(O(n\log^2 n)\)

#include <bits/stdc++.h>

#define FstIO ios::sync_with_stdio(0), cin.tie(0)
#define pii pair <ll, ll> 
#define mem(a, v) memset(a, v, sizeof a) 
#define pb push_back 
#define pbk pop_back 
#define fi first 
#define se second 

using namespace std; 
using ll = long long; 
using ull = unsigned long long; 
using ld = long double; 

const ll N = 1e6 + 5, M = 22; 
const ld eps = 1e-6; 
const ll mod = 998344353, inf = 1e18;  

ll n, m, R, P; 
ll w[N]; 
ll siz[N], dep[N], fa[N]; 
ll id[N], son[N], top[N], tot; 
vector <ll> G[N]; 

ll tc[N], tag[N]; 

void pu(ll k) {
	tc[k] = (tc[k << 1] + tc[k << 1 | 1]) % P; 
}
void pd(ll k, ll l, ll r) {
	if (!tag[k]) return ; 
	ll ls = k << 1; 
	ll rs = ls | 1; 
	ll v = tag[k]; 
	ll mid = l + r >> 1;  
	tc[ls] += (mid - l + 1) * v; tc[ls] %= P;  
	tc[rs] += (r - mid) * v; tc[rs] %= P;  
	tag[ls] += v; tag[ls] %= P; 
	tag[rs] += v; tag[rs] %= P; 
	tag[k] = 0; 
}
void upd(ll k, ll l, ll r, ll ql, ll qr, ll v) {
	if (ql <= l && r <= qr) {
		tc[k] += (r - l + 1) * v; tc[k] %= P; 
		tag[k] += v; tag[k] %= P; 
		return ; 
	}
	pd(k, l, r); 
	ll mid = l + r >> 1; 
	if (ql <= mid) upd(k << 1, l, mid, ql, qr, v); 
	if (qr > mid) upd(k << 1 | 1, mid + 1, r, ql, qr, v); 
	pu(k); 
}
ll que(ll k, ll l, ll r, ll ql, ll qr) {
	if (ql <= l && r <= qr) 
		return tc[k]; 
	pd(k, l, r); 
	ll mid = l + r >> 1, res = 0; 
	if (ql <= mid) res += que(k << 1, l, mid, ql, qr); 
	if (qr > mid) res += que(k << 1 | 1, mid + 1, r, ql, qr); 
	res %= P; 
	return res; 
}
void add(ll l, ll r, ll v) {
	upd(1, 1, n, l, r, v); 
} 
ll ask(ll l, ll r) {
	return que(1, 1, n, l, r); 
}

void dfs(ll u, ll fat) {
	dep[u] = dep[fat] + 1; 
	fa[u] = fat; 
	siz[u] = 1; 
	ll mx = 0; 
	for (auto x : G[u]) {
		if (x == fat) continue; 
		dfs(x, u); 
		siz[u] += siz[x]; 
		if (mx < siz[x]) 
			mx = siz[x], son[u] = x; 
	}
}
void dfs2(ll u, ll topu) {
	top[u] = topu; 
	id[u] = ++ tot; 
	if (son[u]) 
		dfs2(son[u], topu); 
	else 
		return ; 
	for (auto v : G[u]) {
		if (v == son[u] || v == fa[u]) continue; 
		dfs2(v, v); 
	}
}
void addPath(ll u, ll v, ll w) {
	while (top[u] != top[v]) {
		if (dep[top[u]] < dep[top[v]]) swap(u, v); 
		add(id[top[u]], id[u], w); 
		u = fa[top[u]]; 
	}
	if (dep[u] > dep[v]) swap(u, v); 
	add(id[u], id[v], w); 
}
ll askPath(ll u, ll v) {
	ll res = 0; 
	while (top[u] != top[v]) {
		if (dep[top[u]] < dep[top[v]]) swap(u, v); 
		res += ask(id[top[u]], id[u]); 
		res %= P; 
		u = fa[top[u]]; 
	}
	if (dep[u] > dep[v]) swap(u, v); 
	res += ask(id[u], id[v]); 
	res %= P; 
	return res; 
}
void addSon(ll u, ll w) {
	add(id[u], id[u] + siz[u] - 1, w); 
}
ll askSon(ll u) {
	return ask(id[u], id[u] + siz[u] - 1); 
}

signed main() {
//	freopen(".in", "r", stdin); 
//	freopen(".out", "w", stdout); 
	
	FstIO; 
	
	cin >> n >> m >> R >> P; 
	for (ll i = 1; i <= n; ++ i ) cin >> w[i]; 
	for (ll i = 1; i < n; ++ i ) {
		ll u, v; cin >> u >> v; 
		G[u].pb(v); G[v].pb(u); 
	}
	dfs(R, 0); 
	dfs2(R, R); 
	for (ll i = 1; i <= n; ++ i ) add(id[i], id[i], w[i]); 
	while (m -- ) {
		ll op; cin >> op; 
		if (op == 1) {
			ll x, y, z; cin >> x >> y >> z; 
			addPath(x, y, z); 
		}
		if (op == 2) {
			ll x, y; cin >> x >> y; 
			cout << askPath(x, y) << '\n'; 
		}
		if (op == 3) {
			ll u, w; cin >> u >> w; 
			addSon(u, w); 
		}
		if (op == 4) {
			ll u; cin >> u; 
			cout << askSon(u) << '\n'; 
		}
	}
	
	return 0; 
}

P3038

边转点板子。
具体地,把每条边的权值下放到深度更大的那个端点。

长链剖分

另一种剖分方式,每次选择子树内深度最大的儿子作为重儿子,那么划分成若干重链。

P5903 / 求 \(k\) 级祖先

考虑长链剖分,对于一条长度为 \(L\) 的长链,顶端为 \(top\)。你从 \(top\) 分别往上、下预处理 \(L\) 级祖先 / 儿子 \(U_{top,L}\)\(D_{top,L}\)
在跳的时候,先跳到 \(x\) 第一个满足 \(k' = 2^? \le k\)\(k'\) 级祖先 \(fa\),假设剩下还剩 \(r = k - k'\) 级,容易证明 \(fa\) 所在长链的长度必定 \(> r\),于是可以在预处理出来的表里查。那么做到 \(O(n\log n) - O(1)\)

posted @ 2026-09-17 11:50  RainyRadio  阅读(5)  评论(0)    收藏  举报