虚树 学习笔记
虚树 Virtual Tree 学习笔记
引入
题目大意:给一棵
先考虑一个朴素的 DP,每次询问扫一遍整个树。
设
若
否则
时间复杂度
发现
这时可以用虚树来优化。
什么是虚树?
虚树常见于树形 DP 中。
如果一次询问所包含的关键点很少(关键点可以理解为对答案有实际影响的点),即有很多点都是多余无用的。
如果每次我们还是暴力地扫一遍整棵树,那是
我们考虑把关键点及其有关的点取出来,按照原树的祖先后代关系新建一棵树,这就是虚树。
如果所有询问的关键点总数与
如何建一棵虚树?
前置知识
我们需要一个高效的 LCA 算法;
选点
显然我们需要的点是那些关键点和他们之间的 LCA。
根据前人的智慧加上我自己的感性理解,先把所有关键点按 dfs 序排好然后相邻两个求 LCA。
这些 LCA + 根节点 + 所有
所有点的个数就是
连边
先把所有点按 dfs 序排序,然后去重。
我们用一个单调栈维护虚树上由根节点出发的一条链。
开始把根节点 1 入栈。
之后遍历排好序的点。
设当前点为
若
若
回顾连边
当
而与
也就是说在排完序后,只需连
也就是说我们实际并不需要一个栈来维护链。
总结
- 把关键点按 dfs 序排序。
- 关键点相邻两个求 LCA。
- 所有点(LCA + 关键点)按 dfs 序排序。
- 遍历数组,连
, 。
建完虚树后再在虚树上做开头的 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"; } }
套路
数据范围中看见类似
但是建虚树人人都会,重点是如何计算。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话