动机:给定一棵树,每次查询给出 个点,要求选出一棵子树可以覆盖这 个点,且满足子树的边权和最小。
我们的想法是构建出一棵「子树」,保留原树的形态,但是只留下这 个点(还有一些用于连接的 lca),然后求这棵「子树」的边权和就是答案。建树的过程就是在构建「虚树」。
构建虚树的思想参考 OI Wiki
下面给出一个利用虚树思想解决上述问题的的代码,题源 LC3553
cpp
class Solution {
    static const int MAXN = 100000;
    int n, LOG;
    vector<vector<pair<int,int>>> adj;
    vector<array<int,17>> fa;   // 根据 MAXN 选合适的 log₂n 上界
    vector<int> dep, dfn, out;
    vector<ll> dist;
    int timer;
    
    void dfs(int u, int p){
        dfn[u] = ++timer;
        fa[u][0] = p;
        for(auto &e: adj[u]){
            int v = e.first, w = e.second;
            if (v == p) continue;
            dep[v] = dep[u] + 1;
            dist[v] = dist[u] + w;
            dfs(v, u);
        }
        out[u] = timer;
    }
    
    bool isAncestor(int u, int v){
        return dfn[u] <= dfn[v] && out[u] >= out[v];
    }
    
    int lca(int u, int v){
        if (dep[u] < dep[v]) swap(u, v);
        int diff = dep[u] - dep[v];
        for(int j = 0; j < LOG; j++){
            if (diff >> j & 1) u = fa[u][j];
        }
        if (u == v) return u;
        for(int j = LOG-1; j >= 0; j--){
            if (fa[u][j] != fa[v][j]){
                u = fa[u][j];
                v = fa[v][j];
            }
        }
        return fa[u][0];
    }
    
public:
    vector<ll> minimumWeight(int _n, vector<vector<int>>& edges, vector<vector<int>>& queries) {
        n = _n;
        // 计算 LOG 上界
        LOG = 0; while((1<<LOG) <= n) LOG++;
        adj.assign(n, {});
        for(auto &e: edges){
            int u = e[0], v = e[1], w = e[2];
            adj[u].push_back({v,w});
            adj[v].push_back({u,w});
        }
        fa.assign(n, array<int,17>{});
        dep.assign(n, 0);
        dfn.assign(n, 0);
        out.assign(n, 0);
        dist.assign(n, 0);
        timer = 0;
        dfs(0, 0);
        // 构建倍增
        for(int j = 1; j < LOG; j++){
            for(int u = 0; u < n; u++){
                fa[u][j] = fa[ fa[u][j-1] ][j-1];
            }
        }
        
        vector<ll> ans;
        ans.reserve(queries.size());
        vector<int> vs;
        for(auto &pts: queries){
            vs = pts;
            // 1. 按 dfn 排序去重
            sort(vs.begin(), vs.end(), [&](int a, int b){ return dfn[a] < dfn[b]; });
            vs.erase(unique(vs.begin(), vs.end()), vs.end());
            // 2. 插入邻接对的 LCA
            int m = vs.size();
            for(int i = 0; i+1 < m; i++){
                vs.push_back(lca(vs[i], vs[i+1]));
            }
            // 3. 再次排序去重
            sort(vs.begin(), vs.end(), [&](int a, int b){ return dfn[a] < dfn[b]; });
            vs.erase(unique(vs.begin(), vs.end()), vs.end());
            // 4. 构建虚树并累加答案
            ll res = 0;
            vector<int> stk;
            stk.reserve(vs.size());
            stk.push_back(vs[0]);
            for(int i = 1; i < (int)vs.size(); i++){
                int u = vs[i];
                while(!stk.empty() && !isAncestor(stk.back(), u)){
                    stk.pop_back();
                }
                int p = stk.back();
                // p 是 u 的在虚树中的父节点
                res += dist[u] - dist[p];
                stk.push_back(u);
            }
            ans.push_back(res);
        }
        return ans;
    }
};把所有查询点和它们的 LCA 排序去重之后已经得到了虚树中的所有节点,那么只需要找到每个点在虚树中的祖先,然后把链长加入答案即可
关于建虚树的方法,见链接
