动机:给定一棵树,每次查询给出 个点,要求选出一棵子树可以覆盖这 个点,且满足子树的边权和最小。
我们的想法是构建出一棵「子树」,保留原树的形态,但是只留下这 个点(还有一些用于连接的 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 排序去重之后已经得到了虚树中的所有节点,那么只需要找到每个点在虚树中的祖先,然后把链长加入答案即可
关于建虚树的方法,见链接