树
树上启发式合并
这个东西可以解决一些静态,只关心子树贡献的问题。
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)\)。
浙公网安备 33010602011771号