虚树学习笔记
虚树学习笔记
虚树,顾名思义,是虚拟的树。
在关于树的问题中,虚树起到缩小题目规模,优化算法的作用。
算法思路
引入
设
如果
如果
其中
每次询问都去跑一次这样的
关键点
其实很多点是没有用的,如下图:

如果选择的关键点是:

那么我们只需要保证
我们的红色的点的个数级别是
总的来说,浓缩信息,大树变小树。
虚树
于是有了虚树这个概念。
我们先直观的看一些虚树的样子:
由于任意两个节点的
而且虚树中的祖先关系并不会改变,也就是不会出现后代变前辈的伦理问题。
其实不难发现,虚树中只要节点数量足够,且先祖关系不变,那么我们无论添加多少个节点都不影响答案。
当然我们不可能两两枚举关键点去求
为了方便,我们每次会将根加入虚树中。
构造思路1
多个节点
我们先看构造方法:
- 将所有关键点按 dfn 升序排序
- 遍历关键点相邻节点求
,并判重。 - 根据原树中的先祖关系建树。
Q:什么这样子建树可以不重不漏呢?
A:dfn 序相邻的两个节点中间肯定不会存在其他关键点,那么他们两个点的
Q:有没有可能某两组节点的
A:根据 dfn 构造,两个
时间复杂度
构建1 code
void build()
{
sort(a+1,a+k+1,cmp);//a 是关键点数组
int len=0,rlen;
for(int i=1;i<k;i++)
{
int lc=lca(a[i],a[i+1]);
b[++len]=a[i];
b[++len]=lc;
}
b[++len]=a[k];b[++len]=1;
rlen=len;
sort(b+1,b+len+1,cmp);
for(int i=1;i<len;i++)
{
if(b[i]==b[i+1]){rlen--;continue;}
int lc=lca(b[i],b[i+1]);
Tr.add(lc,b[i+1],dis(lc,b[i+1]));
Tr.add(b[i+1],lc,dis(lc,b[i+1]));
}
}
构造方法2
这个方法使用单调栈,这个单调栈维护一个序列,这个序列是一条自浅到深的链(其中的 dfn 序是递增的)。
先将节点按 dfn 序排序,每次加入节点时,我们求栈顶节点与当前节点的
分为以下两种情况:
- 如果他们的
就是栈顶那么当前节点直接入栈。 - 如果他们的
不是栈顶弹出栈中比 的 dfn 序大的节点,再一次入栈 和当前节点。
出栈时都要和栈顶节点建边。
注意:如果新的旧节点出栈后,可能需要连接的节点是
Q:为什么这样子建树也可以不重不漏呢?
A:类似于上面的分析,每次
构建2 code
void build()
{
sort(a+1,a+len+1,cmp);
st[tp=1]=1;E[1].clear();
for(int i=1;i<=k;i++)
{
if(a[i]==1) continue;
int lc=lca(st[tp],a[i]);
if(st[tp]!=lc)
{
while(dfn[lc]<dfn[st[tp-1]])
E[st[tp-1]].push_back(st[tp]),tp--;
if(dfn[lc]>dfn[st[tp-1]])
E[lc].clear(),E[lc].push_back(st[tp]),st[tp]=lc;
else
E[lc].push(sta[tp--]);
}
E[a[i]].clear();st[++tp]=a[i];
}
for(int i=1;i<tp;i++)
E[st[i]].push_back(st[i+1]);
}
实现过程中,将次大 dfn 序比较,简化了特判手段。
回到引入
part 1 建原树
建出原树,求得 LCA 等各种所需的关系。
part 2 建虚树
建出虚树。
part 3 求答案
把我们的
CODE
#include<bits/stdc++.h>
using namespace std;
const int maxn=3e5+5;
struct Edge
{
int tot;
int head[maxn];
struct edgenode{int to,nxt,val;}edge[maxn*2];
inline void add(int u,int v,int z)
{
tot++;
edge[tot].to=v;
edge[tot].nxt=head[u];
edge[tot].val=z;
head[u]=tot;
}
}G,Tr;
int n,m,dfncok;
int f[maxn][25],d[maxn][25],deep[maxn],a[maxn],b[maxn],dfn[maxn];
bool vis[maxn];
bool cmp(int x,int y){return dfn[x]<dfn[y];}
inline void dfs(int u)
{
dfn[u]=++dfncok;
for(int i=G.head[u];i;i=G.edge[i].nxt)
{
int v=G.edge[i].to;
if(v==f[u][0]) continue;
deep[v]=deep[u]+1;
f[v][0]=u;
d[v][0]=G.edge[i].val;
for(int j=1;j<=20;j++) f[v][j]=f[f[v][j-1]][j-1],d[v][j]=min(d[v][j-1],d[f[v][j-1]][j-1]);
dfs(v);
}
}
inline int lca(int x,int y)
{
if(deep[x]<deep[y]) swap(x,y);
for(int i=20;i>=0;i--) if(deep[f[x][i]]>=deep[y]) x=f[x][i];
if(x==y) return x;
for(int i=20;i>=0;i--) if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i];
return f[x][0];
}
inline int dis(int x,int y)
{
int sum=1e6;
if(deep[x]<deep[y]) swap(x,y);
for(int i=20;i>=0;i--) if(deep[f[x][i]]>=deep[y]) sum=min(sum,d[x][i]),x=f[x][i];
return sum;
}
bool cis[maxn];
long long dp[maxn];
inline void dfsdp(int u,int f)
{
for(int i=Tr.head[u];i;i=Tr.edge[i].nxt)
{
int v=Tr.edge[i].to;
if(v==f) continue;
dfsdp(v,u);
}
for(int i=Tr.head[u];i;i=Tr.edge[i].nxt)
{
int v=Tr.edge[i].to;
if(v==f) continue;
if(cis[v]) dp[u]+=Tr.edge[i].val;
else dp[u]+=min(dp[v],1ll*Tr.edge[i].val);
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<n;i++)
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
G.add(x,y,z);
G.add(y,x,z);
}
memset(d,0x5f,sizeof(d));
deep[1]=1;
dfs(1);
scanf("%d",&m);
while(m--)
{
int k;
scanf("%d",&k);
for(int i=1;i<=k;i++) scanf("%d",&a[i]),cis[a[i]]=1;
sort(a+1,a+k+1,cmp);
int len=0,rlen;
for(int i=1;i<k;i++)
{
int lc=lca(a[i],a[i+1]);
b[++len]=a[i];
b[++len]=lc;
}
b[++len]=a[k];
b[++len]=1;
rlen=len;
sort(b+1,b+len+1,cmp);
for(int i=1;i<len;i++)
{
if(b[i]==b[i+1]){rlen--;continue;}
int lc=lca(b[i],b[i+1]);
Tr.add(lc,b[i+1],dis(lc,b[i+1]));
Tr.add(b[i+1],lc,dis(lc,b[i+1]));
}
dfsdp(1,0);
Tr.tot=0;
printf("%lld\n",dp[1]);
for(int i=1;i<=len;i++) Tr.head[b[i]]=0,cis[b[i]]=0,dp[b[i]]=0,vis[b[i]]=0;
}
}
小结
对于虚树类问题的常见解法:
1.在原树上解决问题的时间复杂度,在一次询问的可接受范围内。
2.存在关键点,且关键点个数在可接受范围内。
3.建虚树,将原树问题转为虚树问题。
总之虚树的回头率还是很高的。
例题
例一 P4606 SDOI2018 战略游戏
先建出圆方树,需要统计任意关键点路径上的点数(去重)。
考虑建虚树,求虚树上父亲儿子两点之间的圆点个数即可。
(这里不用真正连边,利用父子关系统计即可)
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5;
struct Edge
{
int tot;
int head[maxn];
struct edgenode{int to,nxt;}edge[maxn*2];
void add(int x,int y)
{
tot++;
edge[tot].to=y;
edge[tot].nxt=head[x];
head[x]=tot;
}
void clr()
{
memset(head,0,sizeof(head));
memset(edge,0,sizeof(edge));
tot=0;
}
}Tr,G;
int n,m,cok,tp,tx;
int dfn[maxn],low[maxn],deep[maxn],f[maxn][25],st[maxn],len[maxn],wz[maxn],ed[maxn];
void tarjin(int u)
{
dfn[u]=low[u]=++cok;
st[++tp]=u;
for(int i=G.head[u];i;i=G.edge[i].nxt)
{
int v=G.edge[i].to;
if(!dfn[v])
{
tarjin(v);
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u])
{
Tr.add(++tx,u);
Tr.add(u,tx);
int x=0;
do
{
x=st[tp--];
Tr.add(tx,x);
Tr.add(x,tx);
}while(x!=v);
}
}
else low[u]=min(low[u],dfn[v]);
}
}
void dfs(int u)
{
len[u]+=(u<=n);
wz[u]=++cok;
for(int i=Tr.head[u];i;i=Tr.edge[i].nxt)
{
int v=Tr.edge[i].to;
if(!deep[v])
{
deep[v]=deep[u]+1;
f[v][0]=u;
len[v]=len[u];
for(int j=1;j<=20;j++) f[v][j]=f[f[v][j-1]][j-1];
dfs(v);
}
}
ed[u]=cok;
}
int Lca(int u,int v)
{
if(deep[u]<deep[v]) swap(u,v);
for(int i=20;i>=0;i--) if(deep[f[u][i]]>=deep[v]) u=f[u][i];
if(u==v) return u;
for(int i=20;i>=0;i--) if(f[u][i]!=f[v][i]) u=f[u][i],v=f[v][i];
return f[u][0];
}
int a[maxn];
bool vis[maxn];
bool cmp(int x,int y){return wz[x]<wz[y];}
bool isfa(int u,int v){return st[u]<st[v]&&ed[u]>=ed[v];}
int main()
{
int _;
scanf("%d",&_);
while(_--)
{
memset(deep,0,sizeof(deep));
Tr.clr();
G.clr();
memset(dfn,0,sizeof(dfn));
memset(low,0,sizeof(low));
memset(f,0,sizeof(f));
memset(st,0,sizeof(st));
memset(len,0,sizeof(len));
memset(wz,0,sizeof(wz));
memset(ed,0,sizeof(ed));
cok=tp=tx=0;
scanf("%d%d",&n,&m);
tx=n;
for(int i=1;i<=m;i++)
{
int u,v;
scanf("%d%d",&u,&v);
G.add(u,v);
G.add(v,u);
}
tarjin(1);
deep[1]=1;
len[1]=1;
cok=0;
dfs(1);
int p;
scanf("%d",&p);
while(p--)
{
int s;
scanf("%d",&s);
for(int i=1;i<=s;i++) scanf("%d",&a[i]),vis[a[i]]=1;
sort(a+1,a+s+1,cmp);
int sl=s;
for(int i=1;i<s;i++)
{
int lca=Lca(a[i],a[i+1]);
if(!vis[lca])
{
vis[lca]=1;
a[++sl]=lca;
}
}
sort(a+1,a+sl+1,cmp);
int ans=-2*s;
for(int i=1;i<=sl;i++)
{
int u=a[i],v=a[i%sl+1];
ans+=len[u]+len[v]-2*len[Lca(u,v)];
}
if(Lca(a[1],a[sl])<=n) ans+=2;
printf("%d\n",ans/2);
for(int i=1;i<=sl;i++) vis[a[i]]=0;
}
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!