维护树上编号连续节点构成连通块/虚树的一种方法

作者:

在

,

维护连通块

现有一棵 $n$ 个节点的树。我们构建一最小的连通块 $S(l,r)$,包含编号在 $[l,r]$ 中的所有节点。多次询问,每次对于任意一对 $[l,r]$,询问关于这连通块的信息。

如果可以离线/可持久化,此类问题有一通用思路:用扫描线维护“$r$ 固定时不同 $l$ 构成连通块的信息”。若 $[p,q]\subseteq[l,r]$,则 $S(l,r)\supseteq S(p,q)$;对于一固定的 $r$,我们维护 $l$ 分别取 $r,r-1,\cdots,1$(向左拓展)时,连通块的增量。对于点 $x$,我们记录 $t_r(x)$ 表示 $r$ 固定时使得 $x\in S(l,r)$ 的最大的 $l$。假设已经对于 $r$ 维护好了;现在拓展到 $r’=r+1$。

若 $x\notin \{r,r+1\}$,则 $t_{r+1}(x)\neq t_{r}(x)$ 是

  • $x$ 在 $r$ 到 $r+1$ 的简单路径上,且
  • $t_{r+1}(x)=r$

的充分必要条件。

考虑任意 $l$。由于 $S$ 是极小的,故将 $r+1$“加入”$S(l,r)$ 得到 $S(l,r+1)$,只需找到 $r+1$ 和连通块在树上最接近节点之间的路径,将路径上所有点加入连通块;由树的性质,此路径是通向 $[l,r]$ 中任意节点的必经之路,也即 $r,r+1$ 的最短路径的某一前缀。又当 $l=r$ 时,$S(l,r+1)$ 就是该路径本身;再有 $[p,q]\subseteq[l,r]\Longrightarrow S(l,r)\supseteq S(p,q)$,故本命题得证。

因此,我们需将整条路径上的点的最大出现时间整体覆盖成 $t_{r+1}(x)=r$($r+1$ 本身除外,它是 $t_{r+1}(r+1)=r+1$)。可以使用珂朵莉树和树剖维护连续的 $t(x)$ 相同的一段。设 $Φ$ 表示连续段个数。每次覆盖操作会对 $\newcommand\bigO{\operatorname{O}}\bigO(\log n)$ 条重链执行“删除若干连续段,加入 $\bigO(1)$ 个连续段”的操作。又 $Φ\leq n$,则 $Φ$ 的总变化量是 $\bigO(n\log n)$ 的。因此若用对数级别有序关联式容器(如 std::map),总时间复杂度为 $\bigO(n\log^2 n)$。

例题

给定大小为 $n$ 的树。$q$ 次询问。给定 $[L,R]$,计算
$$
\sum_{l=L}^R\sum_{r=l}^R |S(l,r)|-1.
$$

实际上是维护边的最大出现时间。如果我们对同一 $r$ 维护所有 $l$ 的 $|S(l,r)|-1$,并称其为序列的一个版本,则求的就是版本号 $r\in[L,R]$ 时序列 $[L,R]$ 内元素的历史版本和。

考虑区间覆盖 $t(x)$ 的过程。对于一个将要被删除的连续段,设其原最大出现时间为 $t_0$,其长度为 $c$。我们将它从 $t_0$ 提升到 $r$,意味着“在 $r+1$ 及以后的版本中,$(t_0,r]$ 中的所有 $l$ 都有多 $c$ 条边在 $S(l,\dots)$ 中”。对 $(t_0,r]$ 的所有点加上 $c$ 即可。使用线段树或树状数组,维护区间加、区间求历史版本和。时间复杂度 $\bigO(n\log^2 n+q\log n)$。

#include <bits/stdc++.h>
using namespace std;

#define inl inline
#define scd second
#define fst first
#define empb emplace_back
#define all(p) begin (p), end (p)
using ll = long long;
using ull = unsigned long long;
using pint = pair <int, int>;

constexpr int N = 1e5 + 10, G = 1<<18;
int n, m, fa[N], siz[N], hson[N], top[N], dep[N], dfn[N], dtot;
ll ans[N];
vector <int> e[N];
vector <pint> qu[N];
map <int, int> g;

void dfs (int x, int fr) {
	dep[x] = dep[fr] + 1;
	siz[x] = 1, fa[x] = fr;
	for (const int y : e[x]) {
		if (y == fr) continue;
		dfs (y, x), siz[x] += siz[y];
		if (siz[y] > siz[hson[x]])
			hson[x] = y;
	}
}
void decomp (int x, int tp) {
	top[x] = tp;
	dfn[x] = ++dtot;
	if (hson[x]) decomp (hson[x], tp);
	for (const int y : e[x])
		if (y != hson[x] && y != fa[x])
			decomp (y, y);
}

struct BIT {
	ll c[N], d[N];
	inl void __add (int x, ll dt) {
		const ll wdt = dt * x;
		for (; x <= n; x += x & -x)
			c[x] += dt, d[x] += wdt;
	}
	inl void add (int l, int r, ll dt) {
		__add (l, dt);
		__add (r + 1, -dt);
	}
	inl ll __ask (int x) const {
		ll s = 0, ws = 0;
		for (int y = x; y > 0; y &= y - 1)
			s += c[y], ws += d[y];
		return s * (x + 1) - ws;
	}
	inl ll ask (int l, int r) const {
		return __ask (r) - __ask (l - 1);
	}
} bit, wbit;

int main () {
	/*  */
	// freopen ("tree.in", "r", stdin);
	// freopen ("tree.out", "w", stdout);

	scanf ("%d", &n);
	for (int i = 1, x, y; i < n; ++i)
		scanf ("%d%d", &x, &y),
		e[x].empb (y), e[y].empb (x);
	dfs (1, 0); decomp (1, 1);
	scanf ("%d", &m);
	for (int i = 0, l, r; i < m; ++i)
		scanf ("%d%d", &l, &r),
		qu[r].empb (l, i);

	g[1] = g[n + 1] = 0;
	for (int x = 2; x <= n; ++x) {
		int u = x, v = x - 1;
		auto idou = [&] (int l, int r, int fr) {
			// 从 t0 = fr - 1 移动到 x - 1。
			const int len = r - l + 1;
			bit.add (max (fr, 1), x - 1, len);
			wbit.add (max (fr, 1), x - 1, (ll) len * x); // 历史版本和拆分常数贡献。
		};
		auto activ = [&] (int l, int r) {
			// 珂朵莉树核心代码。
			auto pt = g.upper_bound (l), it = prev (pt), qt = g.upper_bound (r);
			while (it != qt) {
				const int p = it->fst, q = pt->fst - 1, c = it->scd;
				idou (max (p, l), min (q, r), c);
				g.erase (it); it = pt++;
				if (l > p) g[p] = c;
				if (r < q) g[r + 1] = c;
			}
			g[l] = x;
		};
		while (top[u] != top[v]) {
			const int p = top[u], q = top[v];
			if (dep[p] >= dep[q]) {
				activ (dfn[p], dfn[u]);
				u = fa[p];
			} else {
				activ (dfn[q], dfn[v]);
				v = fa[q];
			}
		}
		if (dep[u] > dep[v]) swap (u, v);
		if (u != v) activ (dfn[u] + 1, dfn[v]);
		const ll s = bit.__ask (x), ws = wbit.__ask (x);
		for (const auto &[l, id] : qu[x])
			ans[id] = (s - bit.__ask (l - 1)) * (x + 1)
				- (ws - wbit.__ask (l - 1));
	}

	for (int i = 0; i < m; ++i)
		printf ("%lld\n", ans[i]);

	return 0;
}

维护虚树

记 $T(l,r)$ 表示 $[l,r]$ 中所有节点构成的虚树。有 $T(l,r)=\{z\mid \exists x,y\in[l,r],\newcommand\lca{\operatorname{lca}}\lca(x,y)=z\}$,自然有 $T(l,r)\subseteq S(l,r)$,且 $[p,q]\subseteq[l,r]\Longrightarrow T(l,r)\supseteq T(p,q)$ 仍成立。我们记 $p_r(x)$ 表示最大的 $l$ 使得 $x\in T(l,r)$。

我们试图通过类似方式维护 $r$ 确定时所有 $T(l,r)$ 的点集。不同于维护连通块,这下并不是 $r+1,r$ 间简单路径上的所有节点都能得到更新,且不一定有 $p_{r+1}(x)=r$。我们需要一些更强的性质,方便精确更新 $p(x)$。

记从 $r+1$ 到 $r$ 的有向简单路径上节点依序为 $y_1,y_2,\cdots,y_k$。则 $\forall i,1\leq i<k,t_{r}(y_i)\leq t_{r}(y_{i+1})$。

仍然考虑 $l=r,r-1,\cdots,1$ 时,$S(l,r)$ 的逐步扩张——每次变成原先的超集。这条性质就很显然了。

记 $z=\lca(x,y)$。记从 $r+1$ 到 $z$ 的有向简单路径上节点依序为 $y_1=r+1,y_2,\cdots,y_k=z$。则 $p_{r+1}(y_i)\neq p_r(y_i)$ 的充分必要条件是

  • $y_i=r+1\lor(y_i=z\land z\neq r)$,或
  • $k>i>1\land t_r(y_{i-1})\neq t_r(y_i)$,即相同 $t_r(y)$ 连续段的最低节点。

$p_{r+1}(r+1)=r+1,p_{r+1}(z)=r$。在 $z$ 到 $r$ 路径子树上的所有节点,其与 $r+1$ 的 $\lca$ 必然为 $z$;在 $z$ 所有祖先的子树内的所有节点,其在 $T$ 中的存在不受到 $r+1$ 的影响。现在只需考虑所有 $y_i$ 受 $r+1$ 加入的影响。

若 $p_{r+1}(y_i)>p_r(y_i)$,则只可能是 $y_i=\lca(r+1,x),x=p_{r+1}(y_i)$,且 $x$ 在 $y_i$ 的与 $r+1$ 不同的子树中,且此时 $x$ 和 $r+1$ 是 $y_i$ 子树中唯二的两个在 $[x,r+1]$ 中的节点。仍考虑 $S(l,r)$ 和 $T(l,r)$ 以 $l$ 递减的顺序逐步扩张。$t_r(y_i)\neq t_r(y_{i-1})$,正说明存在这样一个 $x,x=t_r(y_i)$;且由 $t_r(y_i)$ 的单调性,$x$ 应是 $T(x,r)$ 和 $S(x,r)$ 在 $y_i$ 子树内的唯一一个节点。故而,$r+1$ 的加入使得 $y_i\in T(x,r+1)\backslash T(x,r)$。

仿照上文维护 $t(y)$ 的方式,我们只需在删除相同 $t(y)$ 连续段时,维护其底端节点 $y$ 的 $p(y)$,将其赋为 $t_r(y)$ 即可。时间复杂度不变。

例题

给定以 $1$ 为根的大小为 $n$ 的树。每条边有权值。$q$ 次询问,求 $T(l,r)$ 所有节点有多少种不同的深度。

我们记 $a(x)$ 表示 $x$ 的深度。“将 $p(y)$ 从 $p_0$ 提升到 $t_r(y)$”意味着在 $r+1$ 及以后的版本,$a(y)$ 将在 $l\leq t_r(y)$ 的所有 $T(l,r)$ 中出现。维护每种深度的最大出现时间,若从 $l$ 更新到 $l’$ 则将答案序列的 $(l,l’]$ 区间加一即可。时间复杂度 $\bigO(n\log^2 n+q\log n)$。

#include <bits/stdc++.h>
using namespace std;

#define inl inline
#define scd second
#define fst first
#define empb emplace_back
#define all(p) begin (p), end (p)
using ll = long long;
using ull = unsigned long long;
using pint = pair <int, int>;

constexpr int N = 1e5 + 10, M = 5e5 + 10;
int n, m, tc, ans[M], last[N], seq[N]; ll tt[N], d[N];
int siz[N], hson[N], top[N], fa[N], dep[N], dfn[N], dtot;
int head[N];
struct query { int l, nxt; } qu[M];
vector <pint> e[N];
map <int, int> g;

void dfs (int x, int fr) {
	siz[x] = 1; fa[x] = fr;
	dep[x] = dep[fr] + 1;
	for (const auto &[y, z] : e[x]) {
		if (y == fr) continue;
		d[y] = d[x] + z;
		dfs (y, x); siz[x] += siz[y];
		if (siz[y] >= siz[hson[x]])
			hson[x] = y;
	}
}
void decomp (int x, int tp) {
	top[x] = tp;
	dfn[x] = ++dtot;
	if (hson[x]) decomp (hson[x], tp);
	for (const auto &[y, z] : e[x])
		if (y != fa[x] && y != hson[x])
			decomp (y, y);
}

struct BIT {
	int c[N];
	inl void __add (int x, int dt) {
		for (; x <= n; x += x & -x) c[x] += dt;
	}
	inl void add (int l, int r) {
		__add (l, 1);
		__add (r + 1, -1);
	}
	inl int ask (int x) const {
		int res = 0;
		for (; x > 0; x -= x & -x) res += c[x];
		return res;
	}
} bit;
inl int rev (int x, int n = ::n) { return n - x + 1; }

int main () {
	/*  */
	// freopen ("tree.in", "r", stdin);
	// freopen ("tree.out", "w", stdout);

	scanf ("%d%d", &n, &m);
	for (int i = 1, x, y, z; i < n; ++i) {
		scanf ("%d%d%d", &x, &y, &z);
		e[x].empb (y, z);
		e[y].empb (x, z);
	}

	dfs (1, 0);
	decomp (1, 1);
	memcpy (tt, d + 1, n<<3);
	sort (tt, tt + n);
	tc = unique (tt, tt + n) - tt;
	for (int x = 1; x <= n; ++x) {
		dfn[x] = rev (dfn[x]);
		seq[dfn[x]] = lower_bound (tt, tt + tc, d[x]) - tt;
	}

	for (int i = 1, l, r; i <= m; ++i) {
		scanf ("%d%d", &l, &r);
		if (l == r) ans[i] = 1;
		else qu[i] = { l, head[r] }, head[r] = i;
	}

	g[1] = g[n + 1] = 0;
	for (int x = 2; x <= n; ++x) {
		int u = x, v = x - 1, _c = 0;
		auto activ = [&] (int l, int r, auto escal) {
			auto qt = g.upper_bound (r),
				pt = g.upper_bound (l), it = prev (pt);
			while (it != qt) {
				const int c = it->scd, p = it->fst, q = pt->fst - 1;
				escal (max (l, p), c, _c);
				g.erase (it); it = pt++;
				if (p < l) g[p] = c;
				if (q > r) g[r + 1] = c;
			}
			g[l] = x;
		};
		auto escal = [&] (int idx, int c, int &_c) {
			if (_c == c) return;
			else _c = c;
			const int num = seq[idx], t = c - 1;
			if (last[num] < t) {
				bit.add (rev (t), rev (last[num] + 1));
				last[num] = t;
			}
		};
		while (top[u] != top[v]) {
			if (dep[top[u]] >= dep[top[v]]) {
				activ (dfn[u], dfn[top[u]], escal);
				u = fa[top[u]];
			} else {
				activ (dfn[v], dfn[top[v]], [] (int, int, int&) {});
				v = fa[top[v]];
			}
		}
		if (dfn[u] < dfn[v])
			activ (dfn[u], dfn[v] - 1, escal);
		else if (dfn[v] < dfn[u])
			activ (dfn[v], dfn[u] - 1, [] (int, int, int&) {});
		escal (max (dfn[u], dfn[v]), x, _c);
		escal (dfn[x], x + 1, _c);
		for (int id = head[x]; id; id = qu[id].nxt)
			ans[id] = bit.ask (rev (qu[id].l));
	}

	for (int i = 1; i <= m; ++i)
		printf ("%d\n", ans[i]);

	return 0;
}