知识点 - 虚树
解决问题类型:
利用虚树,可以对于指定多组点集 S S S 的询问进行每组 O ( ∣ S ∣ l o g 2 n + f ( ∣ S ∣ ) ) O(|S|log_2n+f(|S|)) O(∣S∣log2n+f(∣S∣)) 的回答,其中 f ( x ) f(x) f(x)指的是对于树上有 x x x 个点的情况下单组询问这个问题的时间复杂度。可以看到,这个复杂度基本上(除了那个 l o g 2 n log_2n log2n以外)与 n n n 无关了。这样,对于多组询问的回答就可以省去每次询问都遍历一整棵树的 O ( n ) O(n) O(n)复杂度了。
前置知识
- d f s dfs dfs 序的性质以及 l c a lca lca。
先说啥叫虚树,虚树,并不是不存在的树,相反,他是一个大树的一部分,凭借一个虚树,我们可以得知整体的部分信息。如果类比于序列,虚树和大树相当于,子序列和序列,而子树和大树相当于子区间和区间。
例题
题意:给定一棵树,多组询问,每组询问给定 k k k 个点,你可以删掉不同于那 k k k 个点的 m m m个点,使得这 k k k个点两两不连通,要求最小化 m m m,如果不可能输出 − 1 -1 −1。询问之间独立。
数据范围 n ≤ 1 0 5 , ∑ k ≤ 1 0 5 n\leq10^5,\sum k\leq10^5 n≤105,∑k≤105
一看到这种 ∑ k ≤ 1 0 5 \sum k\leq10^5 ∑k≤105的题很可能就是虚树了。
实现
构造方法
先预处理整棵树 l c a lca lca和 d f s dfs dfs序 ,接下来是对于每组询问的构造。
虚树的构建是一个增量算法,要首先将指定的这 k k k个点按照 d f s dfs dfs序排序,然后按照顺序一一加入。可以强行先加入根节点以方便后面的处理。
虚树构建时会开一个栈 s t a c k [ ] stack[] stack[],这个栈本质上和 d f s dfs dfs递归时系统自动开的一个栈原理是一样的,也就是说这个栈保存了从根出发的一条路径(按照深度从小到大储存)。当加入 a [ k ] a[k] a[k]后,满足 s t a c k [ 1 ] = r o o t , s t a c k [ t o p ] = a [ k ] , s t a c k [ x ] 为 s t a c k [ x − 1 ] 后代 stack[1]=root,stack[top]=a[k],stack[x]为stack[x-1]\text{后代} stack[1]=root,stack[top]=a[k],stack[x]为stack[x−1]后代。虚树上 u − > v u->v u−>v 边的连接时间都是在 v v v 被弹出栈时。
考虑如何加入一个新的节点 x x x ,设 z = l c a ( x , s t a c k [ t o p ] ) z=lca(x,stack[top]) z=lca(x,stack[top]),分两类讨论
- z = = s t a c k [ t o p ] z==stack[top] z==stack[top] ,也就是 x x x 是 s t a c k [ t o p ] stack[top] stack[top]的子树内节点。这时直接 s t a c k [ + + t o p ] = x stack[++top]=x stack[++top]=x就好了。
- z ! = s t a c k [ t o p ] z!=stack[top] z!=stack[top]
这种情况中, x x x一定不是 s t a c k [ t o p ] stack[top] stack[top]子树内节点。如图
这是原树上的情况。这时,“…”指代的那些节点以及 s t a c k [ t o p − 1 ] stack[top-1] stack[top−1], s t a c k [ t o p ] stack[top] stack[top]都应弹出栈外(相当于回溯了,开始访问 zz 的另一棵子树)(注意这里 s t a c k [ t o p − 1 ] − > s t a c k [ t o p ] stack[top-1]->stack[top] stack[top−1]−>stack[top]和 z − > x z->x z−>x在原树上不一定直接相连,这里懒得再写个节点而已)
那我们不断弹出 s t a c k [ t o p ] stack[top] stack[top],直到 d e p [ s t a c k [ t o p − 1 ] ] < d e p [ z ] dep[stack[top-1]]<dep[z] dep[stack[top−1]]<dep[z],这时“…”表示的点全部弹完。弹 s t a c k [ t o p ] stack[top] stack[top]都要在虚树上连一条 s t a c k [ t o p − 1 ] − > s t a c k [ t o p ] stack[top-1]->stack[top] stack[top−1]−>stack[top]的边。
注意弹完时可能 s t a c k [ t o p ] ! = z stack[top]!=z stack[top]!=z,我们需要把 z z z补充进虚树中来维护这个,直接加进栈即可。
插入完所有点之后要完全回溯,也就是把栈内节点都弹出,也要连 s t a c k [ t o p − 1 ] − > s t a c k [ t o p ] stack[top-1]->stack[top] stack[top−1]−>stack[top]边。
代码如下,实现起来有一些差别
inline void ins(int x)
{
if (tp==0)
{
st[tp=1]=x;
return;
}
ance=lca(st[tp],x);
while ((tp>1)&&(dep[ance]<dep[st[tp-1]]))
{
add(st[tp-1],st[tp]);
--tp;
}
if (dep[ance]<dep[st[tp]]) add(ance,st[tp--]);
if ((!tp)||(st[tp]!=ance)) st[++tp]=ance;
st[++tp]=x;
}
正确性
对于任意指定两点 a , b a,b a,b的 l c a lca lca,都存在 d f s dfs dfs序连续的两点 u , v ( d f n [ u ] ≤ d f n [ v ] ) u,v(dfn[u]\leq dfn[v]) u,v(dfn[u]≤dfn[v])分别属于 l c a lca lca包含 a , b a,b a,b的两棵子树,此时这 v v v 加入时按照上面的操作必定会把 l c a lca lca加入栈,所以应当加入的点都加入了。对于非 l c a lca lca点,按照上面操作是不会出现这个点的。所以,加入的点恰为所需要的点。
复杂度
每个指定点进栈出栈一次,这部分 O ( ∑ k ) O(\sum k) O(∑k) 。排序和求 l c a lca lca为 O ( ∑ k l o g k ) O(\sum klogk) O(∑klogk)。
此题构造后的做法
构建完成后的使用
以例题为例介绍虚树的使用。首先特判掉无解情况(即一个点和他的父亲都被指定)。构造好虚树后,我们给真正被指定的点的 s i z siz siz设置成 1 1 1(因为有一些加入的点实际上只是 l c a lca lca,要区分开来),然后 d f s dfs dfs这棵虚树。
以下所说的节点 u u u有 s i z siz siz均表示有一个指定节点可以到达 u u u(在执行了下面的删点之后)
对于一个被指定的点 u u u,如果存在孩子 v v v有 s i z siz siz,那么意味着 u − > v u->v u−>v不删点就出现了连通,所以 u − > v u->v u−>v上随便去掉一个点就可以了(这里要 + + a n s ++ans ++ans) ,如果孩子没有 s i z siz siz那就不用处理了。
对于一个未被指定的点 u u u,统计有多少个孩子 v v v有 s i z siz siz。如果只有一个,把 u u u设置成有 s i z siz siz的就好了(相当于看上面的情况决定是否处理)。如果超过一个,那把 u u u 删掉就好了(这里也要 + + a n s ++ans ++ans )。
事实上难点完全在于建虚树。。
算法总复杂度 O ( n + ∑ k l o g 2 k ) O(n+\sum klog_2k) O(n+∑klog2k),如果用非 O ( 1 ) O(1) O(1)的 l c a lca lca要多一个 ∑ k l o g 2 n \sum klog_2n ∑klog2n,如果用倍增 S T ST ST表 l c a lca lca要多一个 n l o g 2 n nlog_2n nlog2n。
注意在最后一遍 d f s dfs dfs虚树时,要把边清空,具体只需要修改头指针(对于 v e c t o r vector vector直接 e r a s e erase erase)就可以了。如果对每个询问暴力 m e m s e t 0 memset0 memset0会导致复杂度退化为 O ( n q ) O(nq) O(nq)
代码
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
const int N=1e5+2,M=2e5+2;
int a[N],lj[M],nxt[M],fir[N],dfn[N],top[N],hc[N],siz[N],f[N],dep[N],st[N];
int n,m,q,i,x,y,c,bs,tp,ance,ans;
inline void read(int &x) {
c=getchar();
while ((c<48)||(c>57))
c=getchar();
x=c^48;
c=getchar();
while ((c>=48)&&(c<=57)) {
x=x*10+(c^48);
c=getchar();
}
}//快读
inline void add(int x,int y) {
lj[++bs]=y;
nxt[bs]=fir[x];
fir[x]=bs;
}//加单向边
void dfs1(int x) {
siz[x]=1;
int i;
for (i=fir[x]; i; i=nxt[i])
if (lj[i]!=f[x]) {
dep[lj[i]]=dep[f[lj[i]]=x]+1;
dfs1(lj[i]);
siz[x]+=siz[lj[i]];
if (siz[hc[x]]<siz[lj[i]])
hc[x]=lj[i];
}
}//树剖预处理
void dfs2(int x) {
dfn[x]=++bs;
if (hc[x]) {
int i;
top[hc[x]]=top[x];
dfs2(hc[x]);
for (i=fir[x]; i; i=nxt[i])
if ((lj[i]!=f[x])&&(lj[i]!=hc[x]))
dfs2(top[lj[i]]=lj[i]);
}
}//树剖预处理
inline int lca(int x,int y) {
while (top[x]!=top[y])
if (dep[top[x]]>dep[top[y]])
x=f[top[x]];
else
y=f[top[y]];
if (dep[x]<dep[y])
return x;
return y;
}//求lca
void qs(int l,int r) { //按照dfs序排序
int i=l,j=r,m=dfn[a[l+r>>1]];
while (i<=j) {
while (dfn[a[i]]<m)
++i;
while (dfn[a[j]]>m)
--j;
if (i<=j)
swap(a[i++],a[j--]);
}
if (i<r)
qs(i,r);
if (l<j)
qs(l,j);
}
inline void ins(int x) {
if (tp==0) {
st[tp=1]=x;
return;
}
ance=lca(st[tp],x);//相当于z
while ((tp>1)&&(dep[ance]<dep[st[tp-1]])) {
add(st[tp-1],st[tp]);
--tp;
}
if (dep[ance]<dep[st[tp]])
add(ance,st[tp--]);
if ((!tp)||(st[tp]!=ance))
st[++tp]=ance;
st[++tp]=x;
}//增量构建
void dfs3(int x) {
int i;
if (siz[x])
for (i=fir[x]; i; i=nxt[i]) {
dfs3(lj[i]);
if (siz[lj[i]]) {
siz[lj[i]]=0;
++ans;
}
} else {
for (i=fir[x]; i; i=nxt[i]) {
dfs3(lj[i]);
siz[x]+=siz[lj[i]];
siz[lj[i]]=0;
}
if (siz[x]>1) {
++ans;
siz[x]=0;
}
}
fir[x]=0;//这里清空
}//对每组询问的解决
int main() {
read(n);
for (i=1; i<n; i++) {
read(x);
read(y);
add(x,y);
add(y,x);
}
bs=0;
dfs1(dep[1]=1);
dfs2(top[1]=1);
memset(fir+1,0,n<<2);
memset(siz+1,0,n<<2);
read(q);
bs=0;
while (q--) {
x=1;
read(m);
for (i=1; i<=m; i++) {
read(a[i]);
siz[a[i]]=1;
}
for (i=1; i<=m; i++)
if (siz[f[a[i]]]) {
puts("-1");
x=0;
break;
}//特判无解
if (!x) {
while (m)
siz[a[m--]]=0;//清空打过的标记
continue;
}
ans=0;
qs(1,m);
if (a[1]!=1)
st[tp=1]=1;//先行添加根节点
for (i=1; i<=m; i++)
ins(a[i]);
if (tp)
while (--tp)
add(st[tp],st[tp+1]);//回溯
dfs3(1);
siz[1]=bs=0;
printf("%d\n",ans);
}
}