虚树学习笔记
虚树简介
考虑每一次 dp 需要的点其实只有关键点本身和两两关键点的 \(\operatorname{LCA}\),所以没必要对整棵树进行 dp。
暴力求时间复杂度为 \(O({k_i}^2)\) 的,那么如何快速求出两两关键点的 \(\operatorname{LCA}\)?将关键点按 dfs 序排序后对相邻两点求 \(\operatorname{LCA}\) 即可。
证明
假设当前序列已经是按 dfs 序排完序的序列,对于一组在序列中相邻的关键点,都可以看做是两颗不同子树中的点,那么对他们求 \(\operatorname{LCA}\) 便可以求出这两颗子树的 \(\operatorname{LCA}\),此时所有在这两颗子树内的点的共同 \(\operatorname{LCA}\) 就求出来了。所以 dfs 序相邻点的 \(\operatorname{LCA}\) 组成的点集一定是包含两两 \(\operatorname{LCA}\) 的点集的。
设加入 \(\operatorname{LCA}\) 后的序列为 \(t\),将 \(t\) 去重后只需要对 \(t\) 按 dfs 序排序后按原图祖先关系建边即可。即 \(\operatorname{LCA(t_i,t_{i+1})} \to t_{i+1}\)。
为什么可以做到不重不漏?
证明
如果 \(x\) 是 y 的祖先,那么 \(x\) 直接到 \(y\) 连边。因为 dfs 序保证了 \(x\) 和 \(y\) 的 dfs 序是相邻的,所以 \(x\) 到 \(y\) 的路径上面没有关键点。
如果 \(x\) 不是 \(y\) 的祖先,那么就把 \(\operatorname{LCA}\) 当作 \(y\) 的的祖先,根据上一种情况也可以证明 \(\operatorname{LCA}\) 到 \(y\) 点的路径上不会有关键点。
所以连接 \(\operatorname{LCA}\) 和 \(y\),不会遗漏,也不会重复。
因为两个点才会产生一个 \(\operatorname{LCA}\),所以时间复杂度是 \(O(m\log n)\) 的。
虚树的建立:
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_u\) 表示使 \(u\) 子树内所有关键点都与 \(1\) 断开的最小代价,令 \(Min_u\) 表示 \(1\) 到 \(u\) 的路径上边权最小值。
虚树建出来就可以了。
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
蠢猪题。
建立出虚树后树上差分一下即可,唯一值得注意的点是 \(1\) 有可能不在要算贡献里,但虚树中又不得不以 \(1\) 做根,所以需要在差分算贡献时特判一下。
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] 大工程
先考虑最小最大值。
定义 \(Min_u\) 和 \(Max_u\) 表示 \(u\) 子树内所有关键点到 \(u\) 的最小/最大值。
答案有两种情况:
-
\(u\) 是关键点,在儿子中找到最小/最大值。
-
\(u\) 不是关键点,找两个儿子的值拼起来即可。
再考虑代价和,其实就是计算每一条边对答案的贡献。
定义 \(dp_u\) 表示 \(u\) 子树内所有关键点到 \(u\) 的距离和,\(g_u\) 表示 \(u\) 子树内关键点个数。
将 \(u\) 到 \(to\) 这一段路径的长度记为 \(w\),\(to\) 儿子的贡献即为 \((g_u-g_{to}) \times (dp_{to}+g_{to} \times w)\),即就是除开 \(to\) 儿子后其他关键点和 \(to\) 子树连边的次数乘上 \(to\) 子树内所有关键点到 \(u\) 的距离和。
建出虚树即可。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
先考虑无解的情况:一个点是关键点且他的父亲也是关键点。
建立出虚树后,还是分两种情况讨论:
-
\(u\) 是关键点,则需要将它与儿子节点的路径断掉。
-
\(u\) 不是关键点,记 \(cnt\) 为它的儿子节点中关键点的数量,若 \(cnt>1\),则将 \(u\) 占领。若 \(cnt=1\),则将 \(u\) 标记为关键点,回溯到父节点去短边。
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] 世界树
毒瘤题。思路来自这里。
不妨先建立出虚树,答案分为两部分求解:
定义 \(g_u\) 表示离 \(u\) 最近的关键点。
显然通过两个 dfs 去更新:第一个 dfs 计算儿子节点中的关键点,第二个 dfs 计算父亲节点的关键点。
关键点子树内的贡献
然后我们可以更新出 \(u\) 的儿子子树中不含关键点的儿子子树的贡献,即为 \(siz_u-\sum siz_{son}\),\(son\) 为 \(u\) 虚树上的儿子这个方向的直接儿子。
两个关键点间路径和及其字数内的节点
对于虚树上的点 \(u,to\) 的路径中的点的贡献,此时可以分为两种情况:
-
\(g_u=g_{to}\),显然这条路径上的点都会为 \(g_u\) 做贡献。
-
二分出断点 \(p,q\),\(p\) 及上半部分属于 \(g_u\),\(q\) 及下半部分属于 \(g_{to}\),大力分讨即可。
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);
}
}

浙公网安备 33010602011771号