虚树学习笔记

虚树类似于树上启发式合并,虽然看起来十分高大,但其原理是十分简单的。

虚树

这里借助模板题来说明P2495 【模板】虚树 / [SDOI2011] 消耗战。

先不看数据范围,这是一道十分基础的树形DP题,我们设 \(dp_i\) 表示使 \(i\) 的所有子树中的资源丰富岛屿均无法到达。枚举每一棵子树的根节点 \(v\),权值为 \(w\),如果节点 \(v\) 是资源丰富点,则当前边是必选的,否则无法使得节点 \(v\) 无法到达,否则这条边并不是必选,权值为 \(\min(dp_v,w)\)。

朴素的 $dp$ 代码

for(auto i:g[u]){
    dfsdp(i,u);
    int w=getmin(i,u);
    if(vis[i])dp[u]+=w;
    else dp[u]+=min(w,dp[i]);
}

如果这道题目不存在 \(m\) 次询问,便可轻易通过,那么接下来我们便可以借虚树的优化来 \(AC\) 这道题目。

虚树,类似于树上启发式合并,看似很难,实则是人类智慧。顾名思义,它是一棵虚拟的树。拿题目的样例举例子:
graph

这里存在关键点:

graph - 副本

发现 \(1\) 只有 \(5\) 这棵子树才是有用的,而 \(9,2\) 则是对答案没有任何贡献。因此我们可以建立一棵虚树,只存在我们需要的关键节点 \(6,10\) 和关键的转折点 \(5\)。组成一棵虚树,边权自然是取最小值,即:

graph

发现得出来的答案是一致的,而且可以极大程度的优化时间。具体的构建方式,我们可以将所有节点按照 \(dfn\) 排序,之后将所有节点即相邻节点的最近公共祖先放进数组,去重后就是点集。但是常数较大,所以可以使用另一种方式进行优化,但也仅仅只存在于常数的优化。

这里给出优化后的代码:

优化代码
for(int i=1;i<=k;i++){
    int lca=LCA(st.top(),s[i]);
    while(st.top()!=1 && lca!=st.top()){
        int t=st.top();
        st.pop();
        if(dfn[st.top()]<dfn[lca])st.push(lca);
        g[st.top()].push_back(t);
    }
    st.push(s[i]);
}
while(st.top()!=1){
    int t=st.top();
    st.pop();
    g[st.top()].push_back(t);
}

即最后在虚树上跑一边树形DP即可达成优化的目的。

当然,一些题目并不是树形DP,比如说P3320 [SDOI2015] 寻宝游戏

这道题目借用虚树的步骤,将节点按照 \(dfn\) 排序之后,则答案就是相邻两个节点的距离之和,因此我们使用一个 \(set\) 来存储当前的点集的 \(dfn\)。当然,如果这道题目给定的是 \(k\) 个节点,也可以使用虚树来优化树形DP,这个树形DP比较简单,判断子树内是否存在关键点,如果存在,则当前的边一定会经过两次,否则不会经过。

代码
void dfsdp(int u){
    dp[u]=0;
    for(auto i:g[u]){
        dfsdp(i);
        dp[u]+=dp[i];
        if(dp[i] || vis[i])dp[u]+=2*gdis(u,i);
    }
    g[u].clear();
    return;
}

20130803173151_tfGc4.thumb.400_0

posted @ 2026-08-09 21:23  tangkaiming  阅读(6)  评论(0)    收藏  举报