虚树学习笔记
虚树简介
考虑每一次 dp 需要的点其实只有关键点本身和两两关键点的
暴力求时间复杂度为
证明
假设当前序列已经是按 dfs 序排完序的序列,对于一组在序列中相邻的关键点,都可以看做是两颗不同子树中的点,那么对他们求便可以求出这两颗子树的 ,此时所有在这两颗子树内的点的共同 就求出来了。所以 dfs 序相邻点的 组成的点集一定是包含两两 的点集的。
设加入
为什么可以做到不重不漏?
证明
如果是 y 的祖先,那么 直接到 连边。因为 dfs 序保证了 和 的 dfs 序是相邻的,所以 到 的路径上面没有关键点。
如果不是 的祖先,那么就把 当作 的的祖先,根据上一种情况也可以证明 到 点的路径上不会有关键点。
所以连接和 ,不会遗漏,也不会重复。
因为两个点才会产生一个
虚树的建立:
il bool cmp(int a,int b){
return id[a]<id[b];
}
il void build(){
t[++num]=1;
sort(a+1,a+k+1,cmp);
for(int i=1;i<k;i++){
t[++num]=a[i];
t[++num]=LCA(a[i],a[i+1]);
} t[++num]=a[k];
sort(t+1,t+num+1,cmp);
num=unique(t+1,t+num+1)-t-1;
for(int i=1;i<num;i++) vec[LCA(t[i],t[i+1])].push_back(t[i+1]);
}
P2495 [SDOI2011] 消耗战
板子题。
考虑暴力:设
虚树建出来就可以了。
dp
部分代码:
il void dfs(int u){
dp[u]=0;
for(auto to:vec[u]){
dfs(to);
dp[u]+=dp[to];
} if(st[u]) dp[u]=Min[u];
else dp[u]=min(dp[u],1ll*Min[u]);
vec[u].clear(); st[u]=false;
}
P6572 [BalticOI 2017] Railway
蠢猪题。
建立出虚树后树上差分一下即可,唯一值得注意的点是
il void dfs(int u){
for(auto to:vec[u]){
if(u!=1||flg) dp[to]++,dp[u]--;
dfs(to);
} st[u]=false,vec[u].clear();
}
P4103 [HEOI2014] 大工程
先考虑最小最大值。
定义
答案有两种情况:
-
是关键点,在儿子中找到最小/最大值。 -
不是关键点,找两个儿子的值拼起来即可。
再考虑代价和,其实就是计算每一条边对答案的贡献。
定义
将
建出虚树即可。dp
代码:
il void dfs(int u){
dp[u]=g[u]=0;
Mindep[u]=INF,Maxdep[u]=-INF;
if(st[u]) g[u]=1,Mindep[u]=Maxdep[u]=0;
for(auto to:vec[u]){
dfs(to);
int w=dep[to]-dep[u];
Min=min(Min,Mindep[u]+Mindep[to]+w);
Max=max(Max,Maxdep[u]+Maxdep[to]+w);
Mindep[u]=min(Mindep[u],Mindep[to]+w);
Maxdep[u]=max(Maxdep[u],Maxdep[to]+w);
g[u]+=g[to],dp[u]+=dp[to]+w*g[to];
} for(auto to:vec[u]){
int w=dep[to]-dep[u];
ans+=1ll*(g[u]-g[to])*(dp[to]+w*g[to]);
} st[u]=false,vec[u].clear();
}
CF613D Kingdom and its Cities
先考虑无解的情况:一个点是关键点且他的父亲也是关键点。
建立出虚树后,还是分两种情况讨论:
-
是关键点,则需要将它与儿子节点的路径断掉。 -
不是关键点,记 为它的儿子节点中关键点的数量,若 ,则将 占领。若 ,则将 标记为关键点,回溯到父节点去短边。
dp
代码:
il int dfs(int u){
int res=0,cnt=0;
for(auto to:vec[u]) res+=dfs(to),cnt+=(st[to]?1:0);
if(st[u]) res+=cnt;
else{
if(cnt>1) res++;
else if(cnt==1) st[u]=true;
} return res;
}
P3233 [HNOI2014] 世界树
毒瘤题。思路来自这里。
不妨先建立出虚树,答案分为两部分求解:
定义
显然通过两个 dfs 去更新:第一个 dfs 计算儿子节点中的关键点,第二个 dfs 计算父亲节点的关键点。
关键点子树内的贡献
然后我们可以更新出
两个关键点间路径和及其字数内的节点
对于虚树上的点
-
,显然这条路径上的点都会为 做贡献。 -
二分出断点
, 及上半部分属于 , 及下半部分属于 ,大力分讨即可。
il void dfs1(int u){
g[u]=-1;
if(st[u]) g[u]=u;
for(auto to:vec[u]){
dfs1(to);
if(g[to]==-1) continue;
if(g[u]==-1) g[u]=g[to];
else{
int d1=get(u,g[u]),d2=get(u,g[to]);
if(d2<d1||(d1==d2&&g[to]<g[u])) g[u]=g[to];
}
}
}
il void dfs2(int u){
for(auto to:vec[u]){
if(g[to]==-1) g[to]=g[u];
else{
int d1=get(to,g[to]),d2=get(to,g[u]);
if(d2<d1||(d1==d2&&g[u]<g[to])) g[to]=g[u];
} dfs2(to);
}
}
il void calc(int u){
ans[g[u]]+=siz[u];
for(auto to:vec[u]){
int w=dep[to]-dep[u]-1;
// u 没有关键点的子树的贡献 :
ans[g[u]]-=siz[plc(to,w)];
// 剩下的情况分类讨论 :
if(g[u]==g[to]) ans[g[u]]+=siz[plc(to,w)]-siz[to];
else{
int d1=get(u,g[u]),d2=get(to,g[to]);
int l=0,r=w,res;
while(l<=r){
int mid=l+r>>1;
if((d1+mid<d2+w+1-mid)||(d1+mid==d2+w+1-mid&&g[u]<g[to])) l=mid+1,res=mid;
else r=mid-1;
} ans[g[u]]+=siz[plc(to,w)]-siz[plc(to,w-res)];
ans[g[to]]+=siz[plc(to,w-res)]-siz[to];
} calc(to);
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!