Loading

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)\)

posted @ 2024-12-09 15:26  Lgx_Q  阅读(41)  评论(0编辑  收藏  举报