虚树 学习笔记

虚树 Virtual Tree 学习笔记

引入

P2495 [SDOI2011] 消耗战

题目大意:给一棵 n 个点的树,m 次询问 k 个点,要求切断一些边使点 1 不可达这些点,求最小切断的边权和。

n2.5105,m5105,k5105


先考虑一个朴素的 DP,每次询问扫一遍整个树。

fi 为使点 i 的子树内的关键点不与 1 连通的最小花费,

gi 为 1 到 i 路径上的边权的最小值。

i 为查询的点,fi=gi

否则 fi=min(gi,fsoni)

时间复杂度 O(nQ)


发现 kn 同阶,所以一次查询里的关键点很少,很多点都是没有用的。

这时可以用虚树来优化。

什么是虚树?

虚树常见于树形 DP 中。

如果一次询问所包含的关键点很少(关键点可以理解为对答案有实际影响的点),即有很多点都是多余无用的。

如果每次我们还是暴力地扫一遍整棵树,那是 O(nQ) 的。

我们考虑把关键点及其有关的点取出来,按照原树的祖先后代关系新建一棵树,这就是虚树。

如果所有询问的关键点总数与 n 同阶,则最后复杂度就是 O(n+Q) 了。

如何建一棵虚树?

前置知识

我们需要一个高效的 LCA 算法;

O(logn) 可以用常数更小的树剖代替倍增。

O(nlogn) 预处理,O(1) 查询的 RMQ 也可。


选点

显然我们需要的点是那些关键点和他们之间的 LCA。

根据前人的智慧加上我自己的感性理解,先把所有关键点按 dfs 序排好然后相邻两个求 LCA。

这些 LCA + 根节点 + 所有 k 个关键点 = 我们所需的点。

所有点的个数就是 O(k) 的。

连边

先把所有点按 dfs 序排序,然后去重。

我们用一个单调栈维护虚树上由根节点出发的一条链。

开始把根节点 1 入栈。

之后遍历排好序的点。

设当前点为 x=di 与栈顶为点 top

LCA(x,top)=top,则点 x 仍在 1topx 这条链上,则连边 (top,x),将 x 直接入栈。

y=LCA(x,top)top,则点 x 不在 1top 这条链上了,y 即为这条链与链 1x 的交叉点。

y 显然也是我们选的点之一,此时一直退栈直到栈顶为 y,然后连边 (y,x),将 x 入栈。

回顾连边

di 连完边后,事实上我们的栈顶一直都为 di

而与 di 连边的点为栈顶 LCA(top,di),即 LCA(di1,di)

也就是说在排完序后,只需连 (LCA(di1,di),di) 即可,看起来十分简洁,其中要连 (1,d1)

也就是说我们实际并不需要一个栈来维护链。

总结

  1. 把关键点按 dfs 序排序。
  2. 关键点相邻两个求 LCA。
  3. 所有点(LCA + 关键点)按 dfs 序排序。
  4. 遍历数组,连 (LCA(di1,di),di)(1,d1)

建完虚树后再在虚树上做开头的 DP 即可。

于是我们的第一道题就迎刃而解了!

代码如下:

#include<bits/stdc++.h>
using namespace std;
int n,m;
const int N=2.5e5+5;
int to[N*2],nx[N*2],st[N],tot,co[N*2];
void add(int u,int v,int w){
to[++tot]=v,nx[tot]=st[u],st[u]=tot,co[tot]=w;
}
vector<int> edge[N];
int top[N],fa[N],son[N],sz[N],d[2*N],dep[N];
#define ll long long
ll f[N],g[N];
int lca(int x,int y){
while(top[x]!=top[y]){
if(dep[top[x]]>dep[top[y]])x=fa[top[x]];
else y=fa[top[y]];
}
if(dep[x]>dep[y])return y;
return x;
}
int dfn[N],dfn1;
void dfs(int x,int y){
sz[x]=1,fa[x]=y,dep[x]=dep[y]+1;
for(int i=st[x];i;i=nx[i]){
int v=to[i];
if(v!=y){
g[v]=min(g[x],(ll)co[i]);
dfs(v,x),sz[x]+=sz[v];
if(sz[v]>sz[son[x]])son[x]=v;
}
}
}
void dfs2(int x){
dfn[x]=++dfn1;
if(son[fa[x]]==x)top[x]=top[fa[x]];
else top[x]=x;
if(son[x])dfs2(son[x]);
for(int i=st[x];i;i=nx[i]){
int v=to[i];
if(v!=fa[x]&&v!=son[x])dfs2(v);
}
}
int K;
int cmp(int x,int y){
return dfn[x]<dfn[y];
}
int bz[N];
void solve(int x){
ll sum=0;
for(int v:edge[x]){
solve(v);
sum+=f[v];
}
edge[x].clear();
if(bz[x])f[x]=g[x];
else f[x]=min(g[x],sum);
bz[x]=0;
}
int main(){
// freopen("1.in","r",stdin);
ios::sync_with_stdio(0);
cin>>n;
for(int i=1;i<n;i++){
int u,v,w;
cin>>u>>v>>w;
add(u,v,w),add(v,u,w);
}
g[1]=1e18;
dfs(1,0);
dfs2(1);
cin>>m;
while(m--){
cin>>K;
for(int i=1;i<=K;i++){
cin>>d[i];
bz[d[i]]=1;
}
sort(d+1,d+1+K,cmp);
int tt=0;
for(int i=1;i<K;i++){
d[i+K]=lca(d[i],d[i+1]);
}
d[K+K]=1;
sort(d+1,d+K+K+1,cmp);
for(int i=1;i<=K+K;i++){
if(d[i-1]!=d[i])d[++tt]=d[i];
}
for(int i=2;i<=tt;i++){
edge[lca(d[i-1],d[i])].push_back(d[i]);
}
solve(1);
cout<<f[1]<<"\n";
}
}

套路

数据范围中看见类似 m105 的东西时就要考虑建虚树了。

但是建虚树人人都会,重点是如何计算。

例题

posted @   dengchengyu  阅读(15)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
点击右上角即可分享
微信分享提示