虚树

虚树

概念

对于一棵树,我们把这棵树上的一些点拿出来,那么这个点集的虚树就是这些点及其在原树上的 lca组成的点集连边所构成的树。举个例子:

o_251013125950_批注 2025-10-13 205935.png (288×228) (cnblogs.com)

建树

考虑用单调栈建树。下文称需要建虚树的这个点集为关键点。

在此之前,我们先将所有关键点按 dfs 序排序。

考虑用单调栈维护虚树上的一条链。首先将第一个关键点直接加入。然后我们依次考虑加入每一个关键点。设当前加入的关键点为 \(x\)。记栈顶的元素是 \(y\),栈中的第二个元素是 \(p\)。记 \(l\)\(lca(x,y)\)

那么我们要加入这个点,就要弹出栈顶的一写元素。就是说,如果 \(dfn_p \le dfn_l\),那么就表明此时可以停止弹栈。

考虑分三类讨论:

  1. \(l = y\) 时,说明 \(y\)\(x\) 的祖先,直接把 \(x\) 加入即可。
  2. \(l=p\) 时,说明 \(l\) 在栈内,那让 \(y\) 出栈,再加入 \(x\) 即可。
  3. \(l \ne p\) 时,说明 \(l\) 没有进栈内,那么需要将 \(y\)\(l\) 连边并将 \(y\) 出栈,然后加入 \(l\)\(x\) 即可。

那么在这个栈中,每个点在虚树上的父亲就是他在栈中的下一个点(更接近栈底的那边)。

o_251013132052_无标题.png (1005×423) (cnblogs.com)

然后就是建出虚树在上面做树形 dp 了。

例题

[P2495 SDOI2011] 消耗战 /【模板】虚树

给出一棵 \(n\) 个点的树以及 \(m\) 次询问,每次询问给出 \(k\) 以及 \(h_i,i \in[1,k],h_i \in[2,n]\)。对于每次询问,要求删掉一些边后 \(1\) 与任意的 \(h_i\) 不连通,求删掉的边的边权和的最小值。\(\sum k \le 5 \times 10^5\)

对于这道题而言,如果只有一次询问,我们可以考虑一个简单的树形 dp \(O(n)\) 求解。

但是加上了 \(m\) 次询问,显然不能每次去 \(O(n)\) 跑。

考虑建立对于 \(h_i\) 建虚树。那么我们实际上得到的信息是没有少掉什么的,该有的都有,并不影响正常的 dp 转移,也没有落下哪一个点。

就相当于把无用的点都去掉了,而且把一次 dp 的时间复杂度降到了 \(O(S)\)\(S\) 为虚树的大小。可以证明,\(S \le 2 \times k\)

#include <bits/stdc++.h>
#define il inline
#define int long long

using namespace std;

const int bufsz = 1 << 20;
char ibuf[bufsz], *p1 = ibuf, *p2 = ibuf;
#define getchar() (p1 == p2 && (p2 = (p1 = ibuf) + fread(ibuf, 1, bufsz, stdin), p1 == p2) ? EOF : *p1++)
il int read() {
	int x = 0; char ch = getchar(); bool t = 0;
	while (ch < '0' || ch > '9') {t ^= ch == '-'; ch = getchar();}
	while (ch >= '0' && ch <= '9') {x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
	return t ? -x : x;
}
bool Beg;
const int INF = 0x3f3f3f3f3f3f3f3f;
const int N = 250000 + 10, M = 20;
int n, m, k, h[N]; 
struct edge {
	int y, w;
};
vector<edge> G[N];
int dfn[N], id[N << 1], tot, mn[N << 1][M], Log2[N << 1];
int dis[N];
il void dfs1(int x, int fa) {
	id[++tot] = x;
	dfn[x] = tot;
	for (edge i : G[x]) {
		if (i.y == fa) continue;
		dis[i.y] = min(dis[x], i.w);
		dfs1(i.y, x);
		id[++tot] = x;
	}
}
il void init() {
	for (int i = 2; i <= tot; i++) Log2[i] = Log2[i / 2] + 1;
	for (int i = 1; i <= tot; i++) mn[i][0] = dfn[id[i]];
	for (int j = 1; j <= Log2[tot]; j++) {
		for (int i = 1; i + (1 << j) - 1 <= tot; i++) {
			mn[i][j] = min(mn[i][j - 1], mn[i + (1 << (j - 1))][j - 1]);
		}
	}
}
il int getlca(int x, int y) {
	if (dfn[x] > dfn[y]) swap(x, y);
	int s = Log2[dfn[y] - dfn[x] + 1];
	return id[min(mn[dfn[x]][s], mn[dfn[y] - (1 << s) + 1][s])];
}
il bool cmp(int a, int b) {return dfn[a] < dfn[b];}
vector<int> E[N];
int st[N], head;
vector<int> Del;
il void build() { // 建虚树
	for (int i : Del) E[i].clear();
	Del.clear();
	head = 0;
	st[++head] = h[1];
	for (int i = 2; i <= k; i++) {
		int x = h[i], y = st[head];
		int l = getlca(x, y);
		if (l == y) {
			st[++head] = x;
			continue;
		}
		while (dfn[st[head - 1]] > dfn[l]) {
			E[st[head - 1]].push_back(st[head]);
			head--;
		}
		if (l != st[head - 1]) {
			E[l].push_back(st[head--]);
			st[++head] = l;
		} else {
			E[l].push_back(st[head--]);
		}
		st[++head] = x;
	}
	for (int i = 1; i < head; i++) {
		E[st[i]].push_back(st[i + 1]);
	}
}
int f[N], a[N];
il void dfs(int x) { // 正常跑树形 dp
	Del.push_back(x);
	f[x] = 0;
	for (int y : E[x]) {
		dfs(y);
		f[x] += f[y];
	}
	if (a[x]) f[x] = dis[x];
	else f[x] = min(f[x], dis[x]);
}
il int solve() {
	k = read();
	for (int i = 1; i <= k; i++) {
		h[i] = read();
		a[h[i]] = 1;
	}
	sort(h + 1, h + 1 + k, cmp);
	build();
	dfs(st[1]);
	printf("%lld\n", f[st[1]]);
	for (int i = 1; i <= k; i++) a[h[i]] = 0;
	
	return 0;
}
bool End;
il void Usd() {cerr << "\nUse: " << (&Beg - &End) / 1024.0 / 1024.0 << " MB" << (double)clock() * 1000.0 / CLOCKS_PER_SEC << "ms\n";}
signed main() {
	n = read();
	for (int i = 1; i < n; i++) {
		int x = read(), y = read(), w = read();
		G[x].push_back({y, w});
		G[y].push_back({x, w});
	}
	dis[1] = INF;
	dfs1(1, 0);
	init();
	int qq = read();
	while (qq--) {
		solve();
	}
	Usd();
	return 0;
}
posted @ 2025-10-13 20:55  Zctf1088  阅读(11)  评论(0)    收藏  举报