虚树的概念
虚树指的是在原树中抽出包含某特定几个节点且保留原树结构的最小的树。
虚树解决的问题
用来解决多组询问的树形 DPDPDP 问题,每次询问会给出一个点集作为关键点,假设点集大小为 kkk,则 ∑k\sum k∑k 和 nnn 同阶。而树形 dpdpdp 的转移往往可以只用关键点来解决,也就是很多非关键点的作用不是必要的。假设给出大小为 kkk 的点集,我们就利用这些关键点建出一棵大小为 O(k)O(k)O(k) 的树,这棵树的结构和原树一致,然后在这颗树上进行 dpdpdp。
建树的复杂度是 O(klogk)O(klogk)O(klogk) 的,dpdpdp 的复杂度可以是 O(k)O(k)O(k),也可以是 O(klogk)O(klogk)O(klogk)。
举个例子
原树

关键点为 {2,4} 的虚树

关键点为 {4,5} 的虚树

关键点为 {4,6} 的虚树

关键点为 {4,6,7} 的虚树

虚树的构建
我们不妨把 111 作为虚树的根节点。
由于虚树的结构要和原树一致,我们基于原树的 dfsdfsdfs 序来完成虚树的构建。
算法流程为:
- 将关键点按 dfsdfsdfs 序排序
- 先将 111 入栈,从 dfsdfsdfs 序小到大添加节点,我们这时候的栈维护的其实是树上的一条链,然后我们分两种情况讨论:
①:栈顶节点和当前节点的 lcalcalca 为栈顶节点,那么直接将当前节点入栈即可
②:栈顶节点和当前节点的 lcalcalca 不是栈顶节点,如图:

这时候,栈维护的链是:

而我们需要把链变成这样:

因此我们把蓝色节点弹栈,并向虚树中的父亲连边:

弹完栈后,如果栈顶元素不是 lcalcalca,应该把 lcalcalca 入栈。
建树的代码
vector<int> G[N];
void build_virtual_tree(int k){
sta[top = 1] = 1; G[1].clear();//先把 1 入栈,并清空 1 的连边
for(int i = 1; i <= k; i++){
if(h[i] != 1){//如果是 1 就没必要重复进栈了
int Lca = lca(sta[top], h[i]);//获得栈顶元素和当前元素的 lca
if(Lca != sta[top]){//如果 lca 不是栈顶元素,即应该换一条链,应该不断弹栈
while(dfn[sta[top - 1]] > dfn[Lca]) G[sta[top - 1]].push_back(sta[top]), top--;//不断连边
if(dfn[Lca] > dfn[sta[top - 1]]) G[Lca].clear(), G[Lca].push_back(sta[top]), sta[top] = Lca;//lca的dfs序大于次大元素,说明lca从未入栈,则清空lca的连边,将lca和栈顶连边并将lca入栈
else G[Lca].push_back(sta[top]), top--;//如果 lca 入过栈了,直接将 lca 和栈顶连边
}
G[h[i]].clear(), sta[++top] = h[i];//将当前元素入栈
}
}
while(top > 1) G[sta[top - 1]].push_back(sta[top]), top--;//将最后一条链连边
}
[SDOI2011] 消耗战
给出 nnn 个点的一棵带有边权的树,以及 qqq 个询问.每次询问给出 kkk 个点,询问这使得这 kkk 个点与 111 点不连通所需切断的边的边权和最小是多少。
其中,n≤2.5×105,q≤5×105,∑k≤5×105,wi≤105n\le2.5\times 10^5,q\le 5\times 10^5,\sum k\le 5\times 10^5,w_i\le 10^5n≤2.5×105,q≤5×105,∑k≤5×105,wi≤105。
如果不建虚树,每次考虑 dpdpdp,设 dpudp_udpu 表示 uuu 的子树中的点全部和 111 断掉联系的答案。
令 valival_ivali 为 iii 到 111 路径上边权的最小值。那么转移的时候分两种情况:
- uuu 是关键点:
dpu=valudp_u=val_udpu=valu - uuu 不是关键点:
dpu=min{valu,∑dpv}dp_u=\min\{val_u,\sum dp_v\}dpu=min{valu,∑dpv}
建虚树后按照同样方式转移即可。总复杂度为 O((∑k)log(∑k))O((\sum k)log(\sum k))O((∑k)log(∑k))
代码如下。
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
#define pii pair<int, int>
const int N = 3e5 + 5;
vector<pii> E[N];
int st[N][20], dfn[N], dep[N], h[N], is[N], sta[N], top, dft;
LL dp[N], val[N];
void dfs(int u, int pre){
dfn[u] = ++dft;
for(auto& e: E[u]){
int v, w;
tie(v, w) = e;
if(v == pre) continue;
dep[v] = dep[u] + 1;
st[v][0] = u;
val[v] = min(val[u], (LL)w);
dfs(v, u);
}
}
void build_st(int n){
for(int i = 1; i <= 19; i++){
for(int j = 1; j <= n; j++) st[j][i] = st[st[j][i - 1]][i - 1];
}
}
int lca(int u, int v){
if(dep[u] < dep[v]) swap(u, v);
int d = dep[u] - dep[v];
for(int i = 19; i >= 0; i--) if(d >> i & 1) u = st[u][i];
if(u == v) return u;
for(int i = 19; i >= 0; i--) if(st[u][i] != st[v][i]) u = st[u][i], v = st[v][i];
return st[u][0];
}
vector<int> G[N];
void build_virtual_tree(int k){
sta[top = 1] = 1; G[1].clear();
for(int i = 1; i <= k; i++){
if(h[i] != 1){
int Lca = lca(sta[top], h[i]);
if(Lca != sta[top]){
while(dfn[sta[top - 1]] > dfn[Lca]) G[sta[top - 1]].push_back(sta[top]), top--;
if(dfn[Lca] > dfn[sta[top - 1]]) G[Lca].clear(), G[Lca].push_back(sta[top]), sta[top] = Lca;
else G[Lca].push_back(sta[top]), top--;
}
G[h[i]].clear(), sta[++top] = h[i];
}
}
while(top > 1) G[sta[top - 1]].push_back(sta[top]), top--;
}
void dfs1(int u, int pre){
LL sum = 0;
dp[u] = 0;//注意每次的 dp 数组要初始化,由于只会用到虚树上的点,对这些点 dfs 的时候初始化即可
for(int& v: G[u]){
if(v == pre) continue;
dfs1(v, u);
sum += dp[v];
}
if(is[u]) dp[u] = val[u];
else dp[u] = min(sum, (LL)val[u]);
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n;
cin >> n;
for(int i = 1, u, v, w; i < n; i++){
cin >> u >> v >> w;
E[u].emplace_back(v, w);
E[v].emplace_back(u, w);
}
val[1] = 1e18;
dfs(1, 0);
build_st(n);
int m;
cin >> m;
while(m--){
int k;
cin >> k;
for(int i = 1; i <= k; i++) cin >> h[i], is[h[i]] = 1;
sort(h + 1, h + k + 1, [&](int x, int y){
return dfn[x] < dfn[y];
}); //按照 dfs 序排序
build_virtual_tree(k);//建虚树
dfs1(1, 0);//在虚树上 dp
cout << dp[1] << '\n';
for(int i = 1; i <= k; i++) is[h[i]] = 0;//把关键点清空
}
return 0;
}
[CF613D] Kingdom and its Cities
给出一棵 nnn 个点的树和 mmm 次询问,每次询问给出 kkk 个点,问最少删掉树上多少个点,使得这 kkk 个点互不连通,无解输出 −1-1−1。
n,m,∑k≤105n,m,\sum k\le 10^5n,m,∑k≤105。
如果存在两个关键点相邻,则肯定无解,否则有解。
然后套路建出虚树,设 dpudp_udpu 表示以 uuu 为根的子树中关键点两两不连通的最小代价,再用一个 szusz_uszu 表示 uuu 的子树中有多少个关键点向 uuu 延伸。
那么,首先有:dpu=∑v∈son(u)dpvdp_u=\sum\limits_{v\in son(u)} dp_vdpu=v∈son(u)∑dpv
如果 uuu 是关键点,那么 uuu 必须和所有的 szusz_uszu 断开,即:dpu+=szu,szu=1dp_u+=sz_u,sz_u=1dpu+=szu,szu=1
如果 uuu 不是关键点,如果 szu>1sz_u>1szu>1,那么必须在 uuu 处断开,即 dpu+=1,szu=0dp_u+=1,sz_u=0dpu+=1,szu=0
代码如下。
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
#define pii pair<int, int>
const int N = 3e5 + 5;
vector<int> E[N];
int st[N][20], dfn[N], dep[N], h[N], is[N], sta[N], top, dft, dp[N], sz[N], flag;
void dfs(int u, int pre){
dfn[u] = ++dft;
for(int& v: E[u]){
if(v == pre) continue;
dep[v] = dep[u] + 1;
st[v][0] = u;
dfs(v, u);
}
}
void build_st(int n){
for(int i = 1; i <= 19; i++){
for(int j = 1; j <= n; j++) st[j][i] = st[st[j][i - 1]][i - 1];
}
}
int lca(int u, int v){
if(dep[u] < dep[v]) swap(u, v);
int d = dep[u] - dep[v];
for(int i = 19; i >= 0; i--) if(d >> i & 1) u = st[u][i];
if(u == v) return u;
for(int i = 19; i >= 0; i--) if(st[u][i] != st[v][i]) u = st[u][i], v = st[v][i];
return st[u][0];
}
vector<int> G[N];
void build_virtual_tree(int k){
sta[top = 1] = 1; G[1].clear();//先把 1 入栈,并清空 1 的连边
for(int i = 1; i <= k; i++){
if(h[i] != 1){//如果是 1 就没必要重复进栈了
int Lca = lca(sta[top], h[i]);//获得栈顶元素和当前元素的 lca
if(Lca != sta[top]){//如果 lca 不是栈顶元素,即应该换一条链,应该不断弹栈
while(dfn[sta[top - 1]] > dfn[Lca]) G[sta[top - 1]].push_back(sta[top]), top--;//不断连边
if(dfn[Lca] > dfn[sta[top - 1]]) G[Lca].clear(), G[Lca].push_back(sta[top]), sta[top] = Lca;//lca的dfs序大于次大元素,说明lca从未入栈,则清空lca的连边,将lca和栈顶连边并将lca入栈
else G[Lca].push_back(sta[top]), top--;//如果 lca 入过栈了,直接将 lca 和栈顶连边
}
G[h[i]].clear(), sta[++top] = h[i];//将当前元素入栈
}
}
while(top > 1) G[sta[top - 1]].push_back(sta[top]), top--;//将最后一条链连边
}
void dfs1(int u, int pre){
dp[u] = sz[u] = 0;
for(int& v: G[u]){
if(v == pre) continue;
dfs1(v, u);
if(is[u] && is[v] && u == st[v][0]) flag = 1;
sz[u] += sz[v];
dp[u] += dp[v];
}
if(is[u]) dp[u] += sz[u], sz[u] = 1;
else{
if(sz[u] > 1) dp[u]++, sz[u] = 0;
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n;
cin >> n;
for(int i = 1, u, v; i < n; i++){
cin >> u >> v;
E[u].push_back(v);
E[v].push_back(u);
}
dfs(1, 0);
build_st(n);
int m;
cin >> m;
while(m--){
int k;
cin >> k;
for(int i = 1; i <= k; i++) cin >> h[i], is[h[i]] = 1;
sort(h + 1, h + k + 1, [&](int x, int y){
return dfn[x] < dfn[y];
});
build_virtual_tree(k);
flag = 0;
dfs1(1, 0);
for(int i = 1; i <= k; i++) is[h[i]] = 0;
if(flag) cout << -1 << '\n';
else cout << dp[1] << '\n';
}
return 0;
}
[HNOI2014] 世界树
给出一棵 nnn 个点的树和 mmm 次询问,每次询问给出 kkk 个点作为关键点,树上的每个点由最近的关键点控制,问每个关键点会控制多少个点。
n,m,k≤3×105n,m,k\le 3\times 10^5n,m,k≤3×105。
套路建出虚树。
先两次 dfsdfsdfs 求出虚树上每个点由哪个点控制。
然后再用一次 dfsdfsdfs 来 dpdpdp,记 ocuoc_uocu 表示控制 uuu 的关键点。
一开始初始化 dpocu=szudp_{oc_u}=sz_udpocu=szu
如果 ocu=ocvoc_u=oc_vocu=ocv,那么 dpocu=dpocu−szvdp_{oc_u}=dp_{oc_u}-sz_vdpocu=dpocu−szv,因为在子树中已经统计过了。
如果 ocu≠ocvoc_u\neq oc_vocu=ocv,那么我们应该从 vvv 往上找到最后一点 ppp,满足 ppp 由 ocvoc_vocv 控制,fapfa_pfap 由 ocuoc_uocu 控制。找 ppp 可以通过倍增求出。然后 dpocu=dpocu−szp,dpocv=dpocv+szp−szxdp_{oc_u}=dp_{oc_u}-sz_p,dp_{oc_v}=dp_{oc_v}+sz_p-sz_xdpocu=dpocu−szp,dpocv=dpocv+szp−szx。
细节颇多,建议读者自己手写一番。。。
代码如下。
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
#define pii pair<int, int>
const int N = 3e5 + 5;
vector<int> E[N];
int st[N][20], dfn[N], dep[N], h[N], re[N], is[N], sta[N], top, dft, sz[N], oc[N];
LL dp[N];
void dfs(int u, int pre){
dfn[u] = ++dft; sz[u] = 1;
for(int& v: E[u]){
if(v == pre) continue;
dep[v] = dep[u] + 1;
st[v][0] = u;
dfs(v, u);
sz[u] += sz[v];
}
}
void build_st(int n){
for(int i = 1; i <= 19; i++){
for(int j = 1; j <= n; j++) st[j][i] = st[st[j][i - 1]][i - 1];
}
}
int lca(int u, int v){
if(dep[u] < dep[v]) swap(u, v);
int d = dep[u] - dep[v];
for(int i = 19; i >= 0; i--) if(d >> i & 1) u = st[u][i];
if(u == v) return u;
for(int i = 19; i >= 0; i--) if(st[u][i] != st[v][i]) u = st[u][i], v = st[v][i];
return st[u][0];
}
int getd(int u, int d){
for(int i = 19; i >= 0; i--) if(d >> i & 1) u = st[u][i];
return u;
}
vector<int> G[N];
void build_virtual_tree(int k){
sta[top = 1] = 1; G[1].clear();
for(int i = 1; i <= k; i++){
if(h[i] != 1){
int Lca = lca(sta[top], h[i]);
if(Lca != sta[top]){
while(dfn[sta[top - 1]] > dfn[Lca]) G[sta[top - 1]].push_back(sta[top]), top--;
if(dfn[Lca] > dfn[sta[top - 1]]) G[Lca].clear(), G[Lca].push_back(sta[top]), sta[top] = Lca;
else G[Lca].push_back(sta[top]), top--;
}
G[h[i]].clear(), sta[++top] = h[i];
}
}
while(top > 1) G[sta[top - 1]].push_back(sta[top]), top--;
}
int getdis(int u, int v){
return dep[u] + dep[v] - 2 * dep[lca(u, v)];
}
void dfs1(int u, int pre){
oc[u] = 0;
dp[u] = 0;
for(int& v: G[u]){
if(v == pre) continue;
dfs1(v, u);
int x = getdis(oc[u], u), y = getdis(oc[v], u);
if(!oc[u] || y < x || (y == x && oc[v] < oc[u])) oc[u] = oc[v];
}
if(is[u]) oc[u] = u;
}
void dfs2(int u, int pre){
for(int& v: G[u]){
if(v == pre) continue;
int x = getdis(oc[v], v), y = getdis(oc[u], v);
if(!oc[v] || y < x || (y == x && oc[u] < oc[v])) oc[v] = oc[u];
dfs2(v, u);
}
}
void dfs3(int u, int pre){
dp[oc[u]] += sz[u];
for(int& v: G[u]){
if(v == pre) continue;
dfs3(v, u);
if(oc[u] == oc[v]) dp[oc[u]] -= sz[v];
else{
int x = getdis(oc[u], u), y = getdis(oc[v], v), w = dep[v] - dep[u] - 1;
int d = w + x - y, k;
k = d / 2;
if(d > 0 && d % 2 && oc[v] < oc[u]) k++;
int p = getd(v, k);
dp[oc[v]] += sz[p] - sz[v];
dp[oc[u]] -= sz[p];
}
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n;
cin >> n;
for(int i = 1, u, v; i < n; i++){
cin >> u >> v;
E[u].push_back(v);
E[v].push_back(u);
}
dfs(1, 0);
build_st(n);
int m;
cin >> m;
while(m--){
int k;
cin >> k;
for(int i = 1; i <= k; i++) cin >> h[i], re[i] = h[i], is[h[i]] = 1;
sort(h + 1, h + k + 1, [&](int x, int y){
return dfn[x] < dfn[y];
});
build_virtual_tree(k);
dfs1(1, 0);
dfs2(1, 0);
dfs3(1, 0);
for(int i = 1; i <= k; i++) is[h[i]] = 0;
for(int i = 1; i <= k; i++) cout << dp[re[i]] << ' ';
cout << '\n';
}
return 0;
}
本文深入探讨了虚树的概念及其在解决多组询问的树形DP问题中的应用。虚树是从原树中抽取包含特定节点的最小树,保持原树结构。通过虚树,可以降低复杂度,例如在[SDOI2011]消耗战问题中,利用虚树进行DP转移,总复杂度为O((∑k)log(∑k))。另外,虚树也在王国和其城市[CF613D]以及世界树[HNOI2014]等题目中起到关键作用,帮助找到最小代价或控制点数量。
6883

被折叠的 条评论
为什么被折叠?



