Top Cluster 树分块入门学习笔记
定义
-
树簇(Cluster):将树上的边划分为若干个连通块,称为树簇。
-
界点、内点:每个树簇内有两个界点,其他点为内点,满足两个树簇至多交于一个界点。
-
簇路径:对于每个树簇,其内部两个界点之间的路径为簇路径。
由于这里不是学习 Top Tree 的地方,所以舍去了某些其他内容。
树簇分块
给定一个常数 \(B\),现在将树划分为若干个树簇,满足每个树簇大小 \(\le B\),并且树簇个数为 \(\mathcal O(B)\)。
下面介绍 Top Cluster 树分块算法,首先,我们规定每个树簇中两个界点为祖先 - 子孙关系。
先 dfs 整棵树,并且开一个栈,维护当前还未被划分进树簇的边(一条边 \((x, fa_x)\) 可以用一个点 \(x\) 来表示)。
根据上面的规定,根节点是其所在簇的界点。对于当前点 \(u\),若满足以下任意一个条件,则将其标记为界点:
-
\(u\) 子树内与 \(u\) 在同一树簇内的界点数量超过 \(1\)。
-
\(u\) 子树内与 \(u\) 在同意树簇内的未被划分的边数 \(\ge B\)。
-
\(u\) 是根节点。
如果 \(u\) 被标记为了界点,此时我们需要将子树内与 \(u\) 连通的边划分为若干个树簇。根据以上分析,我们需要对于每个点 \(u\) 维护两个值 \(siz_u\) 表示当前子树内与 \(u\) 连通的边数,以及 \(ft_u\) 表示当前子树内与 \(u\) 连通的下界点(不存在则 \(ft_u = 0\))。
我们考虑以下算法流程:
-
遍历所有儿子,并维护 \(sz\) 表示当前簇大小,以及 \(ct\) 表示当前簇中下方的界点数量。对于当前儿子,若可以加入当前簇则贪心加入,并实时维护 \(sz\) 和 \(ct\)。
-
若不能加入,则将之前的所有边划分为一个树簇。找出对应的下界点,并对于每个点求出 \(top_u\) 表示 \(u\) 到根的路径中最近的界点,以及 \(near_u\) 表示最近的簇路径上的点。
点击查看代码
void add_cluster(ll u, ll v) {
if(v) {
Fa[v] = u, block[++len] = v, vis[u] = true; // 这里本质上我们就是按照所有界点来给树分块
for(ll x = v; x ^ u; x = fa[x]) vis[x] = true;
}
for(ll i = 1; i <= cur_top; i++) {
ft[cur[i]] = v; cur[i] != v && (top[cur[i]] = u);
if(!v) { near[cur[i]] = u; continue; }
for(ll x = cur[i]; x; x = fa[x])
if(vis[x]) { near[cur[i]] = x; break; }
else if(near[x]) { near[cur[i]] = near[x]; break; }
}
}
void work(ll u, ll v) {
cur_top = 0, ++tot; ll now = 0; // cur 为当前划分进树簇的边,tot 为簇的编号,now 为下界点
do {
cur[++cur_top] = stk[stk_top];
now |= ft[stk[stk_top]], node[tot].pb(stk[stk_top]);
ind[stk[stk_top]] = tot;
} while(stk[stk_top--] != v);
add_cluster(u, now); // 加入一个树簇,界点为 u 和 now
Top[tot] = u, Foot[tot] = now;
}
例题
[Ynoi2009] rpdq
转化为求区间内任意两个点之间的 LCA 深度,莫队二次离线后转化为 \(\mathcal O(n)\) 次到根路径加法,以及查询一个点到根路径权值和。
先 Top Cluster 树分块,修改时,将一条路径分为:
-
当前点到所在簇上界点的路径。
-
上界点到根的路径。
类似于分块,维护 \(bsum_u\) 表示簇内 \(u\) 到达上界点的路径权值和,以及 \(sum_u\) 表示当前簇路径权值和(\(u\) 为当前簇下界点,若 \(u\) 不为界点定义无效),同时维护一个标记 \(tag_u\) 表示当前簇路径加的次数。
查询时,分为三部分:
-
当前点 \(u\) 到 \(near_u\) 的路径。
-
\(near_u\) 到 \(top_u\) 的路径。
-
\(top_u\) 到根的路径。
时间复杂度 \(\mathcal O((n + m) \sqrt n)\)。
点击查看代码
#include <bits/stdc++.h>
namespace Initial {
#define ll int
#define ull unsigned int
#define fi first
#define se second
#define mkp make_pair
#define pir pair <ll, ll>
#define pb emplace_back
#define i128 __int128
using namespace std;
const ll maxn = 2e5 + 10, inf = 1e9, mod = 998244353;
ll power(ll a, ll b = mod - 2) {
ll s = 1;
while(b) {
if(b & 1) s = 1ll * s * a %mod;
a = 1ll * a * a %mod, b >>= 1;
} return s;
}
template <class T>
const ll pls(const T x, const T y) { return x + y >= mod? x + y - mod : x + y; }
template <class T>
void add(T &x, const T y) { x = x + y >= mod? x + y - mod : x + y; }
template <class T>
void chkmax(T &x, const T y) { x = x < y? y : x; }
template <class T>
void chkmin(T &x, const T y) { x = x > y? y : x; }
} using namespace Initial;
namespace Read {
char buf[1 << 22], *p1, *p2;
#define getchar() (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, (1 << 22) - 10, stdin), p1 == p2)? EOF : *p1++)
template <class T>
void rd(T &x) {
char ch; bool neg = 0;
while(!isdigit(ch = getchar()))
if(ch == '-') neg = 1;
x = ch - '0';
while(isdigit(ch = getchar()))
x = (x << 1) + (x << 3) + ch - '0';
if(neg) x = -x;
}
} using Read::rd;
ll B, n, m, bl[maxn]; ull ans[maxn], dis[maxn];
vector <pir> to[maxn];
struct query { ll l, r, id; } q[maxn];
vector <pir> vec1[maxn], vec2[maxn];
struct Data { ll l, r, w, id; }; vector <Data> vec[maxn];
namespace Top_Cluster {
ll near[maxn], fa[maxn], siz[maxn], tot, ind[maxn];
ll top[maxn], ft[maxn], stk[maxn], stk_top, cur[maxn], cur_top;
ll Top[maxn], Foot[maxn], Fa[maxn]; bool vis[maxn];
vector <ll> node[maxn]; ll block[maxn], len;
void add_cluster(ll u, ll v) {
if(v) {
Fa[v] = u, block[++len] = v, vis[u] = true;
for(ll x = v; x ^ u; x = fa[x]) vis[x] = true;
}
for(ll i = 1; i <= cur_top; i++) {
ft[cur[i]] = v; cur[i] != v && (top[cur[i]] = u);
if(!v) { near[cur[i]] = u; continue; }
for(ll x = cur[i]; x; x = fa[x])
if(vis[x]) { near[cur[i]] = x; break; }
else if(near[x]) { near[cur[i]] = near[x]; break; }
}
}
void work(ll u, ll v) {
cur_top = 0, ++tot; ll now = 0;
do {
cur[++cur_top] = stk[stk_top];
now |= ft[stk[stk_top]], node[tot].pb(stk[stk_top]);
ind[stk[stk_top]] = tot;
} while(stk[stk_top--] != v);
add_cluster(u, now);
Top[tot] = u, Foot[tot] = now;
}
void dfs(ll u, ll f = 0) {
fa[u] = f;
for(auto it = to[u].begin(); it != to[u].end(); it++)
if(it -> fi == f) { to[u].erase(it); break; }
siz[u] = 1, ft[u] = 0; ll cnt = 0;
for(pir e: to[u]) {
ll v = e.fi, w = e.se;
dis[v] = dis[u] + w, stk[++stk_top] = v;
dfs(v, u), ft[v] && (++cnt, ft[u] = ft[v]);
siz[u] += siz[v];
} ll now = 0;
if(siz[u] >= B || cnt > 1 || u == 1) {
ll sz = 0, ct = 0;
for(ll i = to[u].size() - 1; ~i; i--) {
ll v = to[u][i].fi, w = to[u][i].se;
ct += ft[v] > 0, sz += siz[v];
if(sz >= B || ct > 1) {
sz = siz[v], ct = ft[v] > 0;
work(u, to[u][i + 1].fi);
}
} work(ft[u] = top[u] = u, to[u][0].fi), siz[u] = 1;
}
}
} using namespace Top_Cluster;
ull tag[maxn], sum[maxn], bsum[maxn], dis_sum[maxn], f_ans[maxn];
void upd(ll u) {
if(u == 1) return;
for(ll i = 1; i < len; i++) sum[block[i]] -= sum[Fa[block[i]]];
if(!Fa[u]) {
sum[ft[u]] += dis[near[u]] - dis[top[u]];
ll c = ind[u];
for(ll v: node[c])
if(!Fa[fa[v]]) bsum[v] -= bsum[fa[v]];
for(; !Fa[u] && u > 1; u = fa[u]) bsum[u] += dis[u] - dis[fa[u]];
for(ll i = node[c].size() - 1; ~i; i--) {
ll v = node[c][i];
if(!Fa[fa[v]]) bsum[v] += bsum[fa[v]];
}
}
for(; u > 1; u = Fa[u]) ++tag[u], sum[u] += dis[u] - dis[Fa[u]];
for(ll i = len - 1; i > 0; i--) sum[block[i]] += sum[Fa[block[i]]];
}
ull ask(ll u) {
ull res = sum[top[u]];
if(!Fa[u]) res += bsum[u];
if(!Fa[u] && ft[u]) res += (dis[near[u]] - dis[top[u]]) * tag[ft[u]];
return res;
}
int main() {
rd(n), rd(m); B = max(1.2, n / sqrt(m));
for(ll i = 1; i <= n; i++) bl[i] = (i - 1) / B + 1;
for(ll i = 1, u, v, w; i < n; i++) {
rd(u), rd(v), rd(w);
to[u].pb(mkp(v, w)), to[v].pb(mkp(u, w));
} dfs(1);
for(ll i = 1; i <= n; i++) dis_sum[i] = dis_sum[i - 1] + dis[i];
for(ll i = 1; i <= m; i++) {
rd(q[i].l), rd(q[q[i].id = i].r);
f_ans[i] = (q[i].r - q[i].l) * (dis_sum[q[i].r] - dis_sum[q[i].l - 1]);
}
sort(q + 1, q + 1 + m, [](const query a, const query b) {
return bl[a.l] ^ bl[b.l]? a.l < b.l : (bl[a.l] & 1? a.r < b.r : a.r > b.r);
});
for(ll i = 1, l = 1, r = 0; i <= m; i++) {
if(r < q[i].r) {
vec1[q[i].r].pb(mkp(1, q[i].id));
vec1[r].pb(mkp(-1, q[i].id));
vec[l - 1].pb((Data) { r + 1, q[i].r, -1, q[i].id });
r = q[i].r;
}
if(l > q[i].l) {
vec[r].pb((Data) { q[i].l, l - 1, 1, q[i].id });
vec2[l - 1].pb(mkp(-1, q[i].id));
vec2[q[i].l - 1].pb(mkp(1, q[i].id));
l = q[i].l;
}
if(r > q[i].r) {
vec1[r].pb(mkp(-1, q[i].id));
vec1[q[i].r].pb(mkp(1, q[i].id));
vec[l - 1].pb((Data) { q[i].r + 1, r, 1, q[i].id });
r = q[i].r;
}
if(l < q[i].l) {
vec[r].pb((Data) { l, q[i].l - 1, -1, q[i].id });
vec2[q[i].l - 1].pb(mkp(1, q[i].id));
vec2[l - 1].pb(mkp(-1, q[i].id));
l = q[i].l;
}
}
for(ll i = 1, sum1 = 0, sum2 = 0; i <= n; i++) {
ll tmp = ask(i); sum1 += tmp, sum2 += dis[i] + tmp;
for(pir t: vec1[i])
ans[t.se] += sum1 * t.fi;
for(pir t: vec2[i])
ans[t.se] += sum2 * t.fi;
upd(i);
for(Data t: vec[i]) {
ull ret = 0;
for(ll j = t.l; j <= t.r; j++) ret += ask(j);
ans[t.id] += ret * t.w;
}
}
for(ll i = 1; i <= m; i++) ans[q[i].id] += ans[q[i - 1].id];
for(ll i = 1; i <= m; i++) printf("%u\n", f_ans[i] - 2 * ans[i]);
return 0;
}
[Ynoi2018] 駄作
考虑 Top Cluster 树分块,对于每个询问,考虑其两个邻域的贡献可以分为同块之间的贡献,以及不同块之间的贡献(每个 Cluster 去掉上界点为一块,根节点特殊处理)。
-
对于同块之间的贡献:
两个邻域在一个块中可以表示为:该块中下界点的邻域,或者该块中上界点的邻域,取决于 \(p_0, p_1\) 在这个块的上面还是下面。
一个块中的邻域只有 \(\mathcal O(B)\) 种可能。拆贡献,\(\text{dist}(u, v) = dep_u + dep_v - 2\cdot dep_{\text{lca}(u, v)}\),每个邻域求出这些点的深度之和。
注意其中有 \(\mathcal O(1)\) 个块不能用上 / 下界点的邻域表示。- 对于不能用上 / 下界点邻域表示的块,可以考虑暴力加入所有第一个邻域的点,然后查询所有第二个邻域的点,复杂度为块的大小,即为 \(\mathcal O(B)\),这部分总时间复杂度为 \(\mathcal O(mB)\)。
- 对于一般的块,第一个邻域放在这个块上有 \(\mathcal O(B)\) 种邻域,然后扫一遍第二个邻域放在这个块上的 \(\mathcal O(B)\) 个邻域,这部分时间复杂度为 \(\mathcal O(\frac nB \cdot B^2) = \mathcal O(nB)\)。
-
对于不同块之间的贡献:
若枚举这两个块,发现统计的贡献形式较为简单,树形 DP 综合这些贡献即可,这部分时间复杂度为 \(\mathcal O(m\cdot \frac nB)\)。
取 \(B = \sqrt n\),时间复杂度 \(\mathcal O((n + m) \sqrt n)\)。