For a set of marked vertices, the virtual (auxiliary) tree keeps plus the LCAs needed to preserve their ancestor structure: at most vertices, each edge standing for a compressed path. Queries with then run a tree DP in each instead of .
Construction: sort by tin, add the LCA of every adjacent pair, sort and deduplicate again; then the virtual parent of each vertex (except the first) is the LCA of it and its predecessor in this order.
pair<vector<int>, vector<int>> virtualTree(const LCA& L, vector<int> vs) { // {vertices by tin, virtual parent (-1 for root)}
auto byTin = [&](int a, int b) { return L.tin[a] < L.tin[b]; };
sort(all(vs), byTin); vs.erase(unique(all(vs)), vs.end());
for (int i = 0, k = sz(vs); i + 1 < k; i++) vs.push_back(L.lca(vs[i], vs[i + 1]));
sort(all(vs), byTin); vs.erase(unique(all(vs)), vs.end());
vector<int> vpar(sz(vs), -1);
for (int i = 1; i < sz(vs); i++) vpar[i] = L.lca(vs[i - 1], vs[i]);
return {vs, vpar}; // edge (vs[i], vpar[i]) has length dep[vs[i]] - dep[vpar[i]]
}Process vertices in reverse order of the returned list for bottom-up DP (children come after parents). Keep per-query arrays indexed by vertex id but reset only the touched vertices, never the whole array. Mark which vertices are original (in ) versus added LCAs.
// example: cut edges of minimum total weight so that no marked vertex is connected to the root (root not marked)
// mn[v] = minimum edge weight on the root path of v (precomputed once)
ll cutFromRoot(const LCA& L, const vector<ll>& mn, vector<int> S, vector<char>& marked, int root) {
S.push_back(root);
auto [vs, vpar] = virtualTree(L, S);
int k = sz(vs); vector<ll> dp(k); map<int, int> idx; for (int i = 0; i < k; i++) idx[vs[i]] = i;
for (int i = k - 1; i > 0; i--) { // vs[0] is the root (smallest tin)
int v = vs[i];
dp[i] = marked[v] ? mn[v] : min(mn[v], dp[i]); // dp[i] held the children's sum
dp[idx[vpar[i]]] += dp[i];
}
return dp[0]; // the root cannot be cut above: sum of children
}Proof: adjacent LCAs suffice, and the parent formula
Lemma: for sorted by
tin, () is the shallowest of , . Indeed all with lie in the subtree of (its preorder interval contains thetinof both ends), so each adjacent LCA is a descendant of ; and the walk must leave the child subtree of containing at some step, whose LCA is then . Hence the set is closed under pairwise LCA (an LCA of LCAs is the LCA of a set of original vertices, i.e. of its min- and max-tinmembers).Parent formula: in the sorted closed set, let . It is an ancestor of and differs from (an ancestor has a smaller
tin). Suppose some lies strictly between and . Then is not an ancestor of (it would be a deeper common ancestor), while is either or lies in an earlier child subtree of ; in both cases , contradicting adjacency. So is the nearest ancestor of in . Size: .Example DP: to separate a marked from the root one must cut an edge on its root path, the cheapest costing ; for an unmarked virtual vertex, either cut above it (, which separates its whole subtree) or solve each virtual child independently (their compressed paths are disjoint below , and a single cut above a child’s path is captured by that child’s own since is a prefix minimum). Marked vertices must be cut above themselves.
Related compression techniques
All of these share the same instinct: replace a big structure with a small one that keeps the one property the query needs.
| Technique | Compresses | Preserves |
|---|---|---|
| Virtual tree | a vertex subset | ancestor relationships |
| Condensation | SCCs | reachability |
| Bridge tree | 2-edge-connected components | edge connectivity |
| Block-cut tree | biconnected components | vertex connectivity |
| Centroid decomposition | the whole tree | path structure, depth |
Typical uses
| Query on a vertex set | Virtual-tree DP |
|---|---|
| minimum weight to separate all marked vertices from the root | the DP above |
| sum / min / max of pairwise distances among marked vertices | edge contributions with marked counts; two best depths |
| number of vertices on the union of paths between marked vertices (Steiner tree size) | sum of virtual edge lengths + 1; equals half the cyclic tour over sorted |
| minimum vertices to delete so marked ones are pairwise disconnected | greedy DP counting marked children |
| for each marked vertex, the nearest other marked vertex | two passes (down then up) on the virtual tree |
Proof: Steiner tree size by the cyclic tour
Sort by
tinand close the cycle . The concatenated paths form the DFS walk of the minimal subtree spanning (consecutive marked vertices in preorder are joined exactly through the part of the DFS between them), which traverses each edge of that subtree twice and no other edge. So the number of edges is , and the vertex count is one more.