知识点 - 虚树

知识点 - 虚树

解决问题类型:

利用虚树,可以对于指定多组点集 S S S 的询问进行每组 O ( ∣ S ∣ l o g 2 n + f ( ∣ S ∣ ) ) O(|S|log_2n+f(|S|)) O(Slog2n+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 n105,k105

一看到这种 ∑ k ≤ 1 0 5 \sum k\leq10^5 k105的题很可能就是虚树了。

实现

构造方法

先预处理整棵树 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[x1]后代。虚树上 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]),分两类讨论

  1. 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就好了。
  2. 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]子树内节点。如图

img

这是原树上的情况。这时,“…”指代的那些节点以及 s t a c k [ t o p − 1 ] stack[top-1] stack[top1], 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[top1]>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 ] ] &lt; d e p [ z ] dep[stack[top-1]]&lt;dep[z] dep[stack[top1]]<dep[z],这时“…”表示的点全部弹完。弹 s t a c k [ t o p ] stack[top] stack[top]都要在虚树上连一条 s t a c k [ t o p − 1 ] − &gt; s t a c k [ t o p ] stack[top-1]-&gt;stack[top] stack[top1]>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 ] − &gt; s t a c k [ t o p ] stack[top-1]-&gt;stack[top] stack[top1]>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 − &gt; v u-&gt;v u>v不删点就出现了连通,所以 u − &gt; v u-&gt;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);
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值