[图论入门]虚树

不知道虚树到底应该放到 “算法” 里还是 “图论” 里...暂且当时图论里的吧。

#1.0 虚树

#1.1 简单介绍

虚树,是对于一棵给定的节点数为 \(n\) 的树 \(T\),构造一棵新的树 \(T'\) 使得总结点数最小且包含指定的某几个节点和他们的 \(\texttt{LCA}\)

一般用来优化树形 \(\texttt{DP}\) 等。比如说,有一颗 \(n\) 个节点的树,只有 \(m\) 个节点必须要考虑(\(m\) 可能远小于 \(n\)),那么我们完全可以不去考虑那些没有任何用处的节点。像是树形 \(\texttt{DP}\),我们产生贡献、积累贡献的地方有可能只有关键点与他们两两之间的 \(\texttt{LCA}\),那么我们只需要存下关键点与 \(\texttt{LCA}\) 即可。

#1.2 实现思路

首先,将关键点按 \(\texttt{dfn}\) 序排序。然后我们需要一条单调栈来维护一条虚树上的链,栈中的 \(\texttt{dfn}\) 序单调递增。这意味着,栈中相邻的节点在虚树上也是相邻的,且栈中一个节点屁股底下的就是自己在虚树中的父亲节点。

首先,为了方便后面处理,我们先强制把根节点加入栈中,那么下面将依次将关键点加入栈。

第一种情况:当前节点与 stack[top]\(\texttt{LCA}\) 就是 stack[top] ,那么说明他们在同一条链上,可以直接加入栈;

第二种情况:当前节点与栈顶元素的 \(\texttt{LCA}\) 不是栈顶元素,那么说明他们不是同一条链上的(分叉了),将栈中的节点弹出并连边,直到 stack[top - 1] 的深度比求得的 \(\texttt{LCA}\) 的深度要小,那么说明异端岔开的链已经只剩栈顶一个了,检查 stack[top - 1] 是否是 \(\texttt{LCA}\),如果不是,就需要将 \(\texttt{LCA}\) 向栈顶连一条边,然后将栈顶弹出,加入 \(\texttt{LCA}\)。就可以将该节点加入栈了。

最后将栈中的节点全部弹出并连边就好了。

注意一点,在虚树建立后,进行操作的最后一个 \(\texttt{DFS}\) 的过程中,要清空邻接表,但不要直接暴力 memset(),那样会使复杂度退化,可以直接将头指针清空,如果是 vector 可以直接 erase()

#1.3 代码实现

inline void ins(int x){ //插入某个关键点
    if (!stp) {st[++ stp] = x;return;} //空栈,直接插入
    int ance = LCA(x,st[stp]);
    /*不在同一条链上的要弹出,在同一条链上的不会进入循环*/
    while (stp > 1 && d[ance] < d[st[stp - 1]])
      add(st[stp - 1],st[stp]),stp --;
    if (d[ance] < d[st[stp]]) add(ance,st[stp --]);
    if (!stp || st[stp] != ance) st[++ stp] = ance;
    st[++ stp] = x;
}

#2.0 例题

#2.1 P2495 [SDOI2011]消耗战

普通的树形 \(\texttt{DP}\) 并不难想,对于一个树上的非关键点 \(x\),只有断掉它连向父亲的边和断掉连向子孙的边两种情况。断上面的情况可以在其父节点处理,所以只用考虑断连向子孙的边。如果 \(x\) 的一个儿子是关键点,那么这条边必断,如果一个儿子不是关键点,考虑是断这个儿子上面还是下面,取最小值。

但是直接做时间复杂度爆炸,注意到 \(\sum k_i\leq5\times10^5\),再发现有许多点没有用处,考虑建虚树处理。

建完虚树后,原来的 \(\texttt{DP}\) 策略似乎不太可行了,注意到在虚树上的点除了关键点就是他们的 \(\texttt{LCA}\),且叶节点均为关键点。

发现,如果一个点是关键点,那么,在它下面的关键点可以不加入虚树,因为在该点之上必然存在删除的边,从而导致下面的点也不联通。

按上面的想法建出虚树,不妨设 \(f(x)\) 表示在虚树上以 \(x\) 为根的子树(不包含 \(x\))中的所有关键点与 \(1\) 号节点断开需要的最小代价,显然有

\[f(x)=\begin{cases}\min\left\{mn(x),\sum\limits_{y\in son_x}f(y)\right\},&|son_x|>0\\mn(x),&|son_x|=0\end{cases} \]

其中 \(mn(x)\) 表示从 \(x\)\(1\) 号节点路径上的最小边权。现在能有子节点的只有 \(\texttt{LCA}\) 了。考虑为什么可以取 \(mn(x)\),对于 \(x\) 的父节点 \(fa_x\),如果连向 \(x\) 的边 \((fa_x,x)\) 的边权大于 \(mn(x)\),那么由于和式其余项相加大于等于 \(0\);所以必然仍取 \(mn(fa_x)\),如果 \((fa_x,x)=mn(x)\),正确性显然。

const int N = 2000010;
const int INF = 0x3fffffff;

struct Edge{
    int u,v;
    ll w;
    int nxt;
};
Edge e[N],ne[N];

int cnt = 1,head[N],n,m,ncnt = 1,nhead[N];
int son[N],dfn[N],T,st[N],stp,f[N];
int fa[N],size[N],d[N],top[N],fl[N];
ll mn[N];

inline ll Min(const ll &a,const ll &b){
    return a < b ? a : b;
}

inline void ADD(const int &u,const int &v,const ll &w){
    e[cnt].u = u;e[cnt].v = v;e[cnt].w = w;
    e[cnt].nxt = head[u];head[u] = cnt ++;
}

inline void add(const int &u,const int &v){
    ne[ncnt].u = u;ne[ncnt].v = v;
    ne[ncnt].nxt = nhead[u];nhead[u] = ncnt ++;
}

inline int cmp(const int &a,const int &b){
    return dfn[a] < dfn[b];
}

inline void dfs1(int x,int _fa){
    size[x] = 1,d[x] = d[_fa] + 1,fa[x] = _fa;
    for (int i = head[x];i;i = e[i].nxt){
        if (e[i].v == fa[x]) continue;
        mn[e[i].v] = Min(mn[x],e[i].w);
        dfs1(e[i].v,x);
        size[x] += size[e[i].v];
        if (size[e[i].v] > size[son[x]])
          son[x] = e[i].v;
    }
}

inline void dfs2(int x,int t){
    top[x] = t;dfn[x] = ++ T;
    if (!son[x]) return;
    dfs2(son[x],t);
    for (int i = head[x];i;i = e[i].nxt){
        if (e[i].v == fa[x] || e[i].v == son[x])
          continue;
        dfs2(e[i].v,e[i].v);
    }
}

inline int LCA(int a,int b){
    while (top[a] != top[b]){
        if(d[top[a]] < d[top[b]])
          swap(a,b);
        a = fa[top[a]];
    }
    if (d[a] < d[b]) swap(a,b);
    return b;
}

inline void ins(int x){
    if (stp == 1) {st[++ stp] = x;return;}
    int ance = LCA(x,st[stp]);
    if (ance == st[stp]) return;
    while (stp > 1 && d[ance] < d[st[stp - 1]])
      add(st[stp - 1],st[stp]),stp --;
    if (d[ance] < d[st[stp]]) add(ance,st[stp --]);
    if (!stp || st[stp] != ance) st[++ stp] = ance;
    st[++ stp] = x;
}

inline ll DP(int x){
    if (!nhead[x]) return mn[x];
    ll res = 0;
    for (int i = nhead[x];i;i = ne[i].nxt)
      res += DP(ne[i].v);
    nhead[x] = 0;
    return Min(mn[x],res);
}

int main(){
    mset(mn,0x3f);scanf("%d",&n);
    for (int i = 1;i < n;i ++){
        int u,v;ll w;
        scanf("%d%d%lld",&u,&v,&w);
        ADD(u,v,w);ADD(v,u,w);
    }
    dfs1(1,0);dfs2(1,1);
    scanf("%d",&m);
    while (m --){
        int k,a[N];ncnt = 1;
        scanf("%d",&k);st[stp = 1] = 1;
        for (int i = 1;i <= k;i ++)
          scanf("%d",&a[i]);
        sort(a + 1,a + k + 1,cmp);
        for (int i = 1;i <= k;i ++) ins(a[i]);
        while (stp) add(st[stp - 1],st[stp]),stp --;
        printf("%lld\n",DP(1));
    }
    return 0;
}

参考资料

[1] 【洛谷日报#185】浅谈虚树 - SSerxhs

[2] 虚树入门 - 自为风月马前卒

[3] 虚树 - OI Wiki

posted @ 2021-06-20 17:00  Dfkuaid  阅读(207)  评论(2编辑  收藏  举报