Virtual Tree

0#原来你的虚树是出芽生殖的嘛?!

消耗战!你消耗的不是别的!你消耗的是我!

虚树

引入

这种东西配合实际例子来讲会比较好。

模板题是这道

下文的「标记点」就是有资源的岛屿。

首先我们可以想到对于每个询问都来一次树形dp,时间复杂度为 $\mathcal{O}(nm)$。

这种复杂度显然不够优秀。

可以发现 $\sum{k} \leqslant 5 \times 10^{5}$,这意味着如果我们每次询问的复杂度是和 $k$ 相关而不是和 $n$ 相关,那我们就可以把复杂度降到 $\mathcal{O}(\sum{k})$!

回想树形dp的过程,我们发现很多未标记点的转移其实可以简化,就比如一条链(链上所有点都是未标记点)的转移是直接取这条链上的最小边权转移。

我们可以尝试留下一些必要的节点形成一棵新的树,而这棵树的大小是和 $k$ 同阶的,再对这棵新树进行树形dp。

来看我们需要在这棵新树保留哪些节点,结合树形dp的过程,可以发现我们需要保留所有标记点和任意两个标记点的最近公共祖先

因为若一个非标记点不是某两个标记点的最近公共祖先,那么它的转移一定可以合并到一条非标记点构成的链上,反之则不能。

这样的最近公共祖先有多少个,答案是最多有 $k - 1$ 个。

我们将标记点按 dfs 序升序排序,取下标相差 $1$ 的节点求它们的 LCA,这些 LCA 就是我们要求的任意两标记点的最近公共祖先构成的集合,具体证明留到晚上来写。

好了,现在我们知道了要保存哪些节点来树形dp了,并且证明了其规模和 $k$ 同阶。

这个简化信息过后的树就叫做虚树。

实现

可以按照上面的说到的做法来,排序,两两求 LCA,再去重。

然后再次按照 dfn 排序,再两两求 LCA,将这两个点和 LCA 连边即可。

证明可以去看 OI wiki,我认为这个证明的意义不是很大。

上面这个方法是0#讲的,但是我觉得不够优雅。

还有一种是我用的单调栈建树。

具体来说,是用单调栈维护虚树上的从根节点开始的一条链,容易发现栈里的 dfn 是单调递增的。

每次弹出的时候将弹出节点和弹出后的栈顶连边。

依次加入排序后的节点。

每次取栈顶节点和当前节点求 LCA,这个 LCA肯定是得加进去的,所以先把 LCA 加进去。

如果栈顶下方节点(也就是 $top - 1$ 的节点)的 dfn 大于 LCA 的 dfn,那么就可以把栈顶弹出了。(因为这个时候栈顶肯定和 LCA 不在一条链上)

直到栈顶下方节点的 dfn 小于等于 LCA 的 dfn 时停止弹出(若等于说明栈顶节点就是 LCA)。

此时要将栈顶节点和 LCA 连边,再把栈顶节点弹出(这个时候弹出就不需要连边了)。

上面的过程相当于给 LCA 找到它所在的位置并加入进去,因为我们是按 dfn 递增顺序加入的,所以跟 LCA 不在一条链上的节点可以直接丢掉。

这个时候如果栈顶不是 LCA 要将 LCA 入栈。

接着将当前节点入栈就好了。

最后要把剩下的栈内节点弹完,它们同样是虚树上的一条链。

给出代码实现(是从消耗战粘过来的,把树链剖分和st表等其它部分删掉了):

struct Virtual_Tree {
    int n, LCA, top, s[250001], nodes[250001];
    bool tag[250001];
    ll dp[250001];
    vector< pair<int, int> > g[250001];
    void add_egde(int u, int v, int w) {
        g[u].push_back(make_pair(v, w));
        g[v].push_back(make_pair(u, w));
    }
    void build() {
        stable_sort(nodes + 1, nodes + 1 + n, [&](int a, int b) -> bool {return dfn[a] < dfn[b];});
        dp[1] = 0, g[1].clear(), s[top = 1] = 1;
        fu(i, 1, n) {
            LCA = lca(s[top], nodes[i]);
            if(LCA != s[top]) {
                while(dfn[s[top - 1]] > dfn[LCA]) {
                    add_egde(s[top - 1], s[top], ask(s[top - 1], s[top]));
                    --top;
                }
                if(LCA != s[top - 1]) {
                    dp[LCA] = 0, g[LCA].clear();
                    add_egde(s[top], LCA, ask(s[top], LCA));
                    s[top] = LCA;
                }
                else {
                    add_egde(s[top - 1], s[top], ask(s[top - 1], s[top]));
                    --top;
                }
            }
            dp[nodes[i]] = 0, tag[nodes[i]] = true, g[nodes[i]].clear(), s[++top] = nodes[i];
        }
        fu(i, 2, top) {
            add_egde(s[i - 1], s[i], ask(s[i - 1], s[i]));
        }
    }
    void dfs(int now, int father) {
        fo(i, g[now]) {
            if(i.first != father) {
                dfs(i.first, now);
                dp[now] += tag[i.first] ? i.second : min((ll)i.second, dp[i.first]);
            }
        }
    }
    ll solve() {
        build();
        dfs(1, 1);
        fu(i, 1, n) tag[nodes[i]] = false;
        return dp[1];
    }
} vt;
posted @ 2023-10-26 17:28  A_box_of_yogurt  阅读(19)  评论(0)    收藏  举报  来源
Document