Given a graph and a set of terminals, find the minimum-weight connected subgraph containing all terminals. It may use extra (“Steiner”) vertices.
Dreyfus-Wagner — exact,
DP over subsets of terminals: dp[mask][v] = the minimum cost of a tree connecting the terminal set mask and containing vertex .
Base: for each terminal .
Two transitions:
- Merge two sub-trees meeting at : .
- Grow by moving the meeting point along an edge — a Dijkstra over the whole graph for each mask, seeded from every vertex with its current
dp[mask][·]value: .
Answer: .
const ll INF = 1e18;
ll steiner(int n, const vector<vector<pair<int, ll>>>& adj, const vector<int>& term) {
int k = term.size(), FULL = 1 << k;
vector<vector<ll>> dp(FULL, vector<ll>(n, INF));
for (int i = 0; i < k; i++) dp[1 << i][term[i]] = 0;
for (int mask = 1; mask < FULL; mask++) {
for (int v = 0; v < n; v++) // 1. merge sub-trees at the same vertex
for (int sub = (mask - 1) & mask; sub; sub = (sub - 1) & mask)
if (dp[sub][v] < INF && dp[mask ^ sub][v] < INF)
dp[mask][v] = min(dp[mask][v], dp[sub][v] + dp[mask ^ sub][v]);
priority_queue<pair<ll, int>, vector<pair<ll, int>>, greater<>> pq; // 2. Dijkstra to move the meeting point
for (int v = 0; v < n; v++) if (dp[mask][v] < INF) pq.push({dp[mask][v], v});
while (!pq.empty()) {
auto [d, u] = pq.top(); pq.pop();
if (d > dp[mask][u]) continue;
for (auto [v, w] : adj[u]) if (d + w < dp[mask][v]) { dp[mask][v] = d + w; pq.push({dp[mask][v], v}); }
}
}
return *min_element(dp[FULL - 1].begin(), dp[FULL - 1].end());
}Complexity: subset enumeration , times vertices ; Dijkstra per mask, masks, ; space . Practical up to - with in the thousands.
Proof: Dreyfus-Wagner is correct
Root any optimal Steiner tree for terminal set
maskat an arbitrary vertex . Two cases, exhaustive:
(a) is a branching point (degree in , with at least two of its subtrees each containing at least one terminal): removing splits into subtrees, and grouping them into any two non-empty sets partitionsmaskintosubandmask \ sub, each spanned by a smaller tree rooted at — exactly the merge transition, minimised over every such split.
(b) has only one subtree containing terminals (degree 1 in , or a pass-through vertex with everything on one side): then restricted to that side, plus the edge from to its one neighbour on that side, is a smaller instance — exactly the grow transition, , and running Dijkstra from all vertices simultaneously finds the optimal choice of (and chains of such moves) in one pass per mask, since the grow step is literally shortest-path relaxation.
Every optimal tree falls into case (a) or (b) at any vertex it contains (in particular, at the root of whatever shape it has), so the DP’s two transitions, applied for every mask in increasing order of popcount, are exhaustive — and by induction onmask(both transitions only reference strictly smaller masks or the Dijkstra relaxation within the same mask, which itself only improves values monotonically to a fixed point), converges to the true optimum.
Tested: steiner checked against brute force (enumerate every vertex subset containing all terminals, take the MST of the induced subgraph if connected, keep the minimum over all subsets) on small random graphs with up to 10 vertices and up to 4 terminals.
Approximation: the metric-closure MST — a 2-approximation
- Compute all-pairs shortest paths among the terminals (one Dijkstra per terminal).
- Build the complete graph on the terminals with those distances.
- Take its MST.
- Expand each MST edge back into its shortest path.
// for each terminal t: dijkstra(t) // k runs, reusing an already-tested Dijkstra
// build the complete graph on terminals with dist[i][j]
// mst = kruskal(that graph) // reusing an already-tested Kruskal
// expand each MST edge into its shortest pathProof: the metric-closure MST is a 2-approximation
Take the optimal Steiner tree (cost ) and double every edge, giving an Eulerian multigraph (every vertex has even degree) of cost that is connected and spans all terminals (plus possibly Steiner vertices). An Eulerian circuit of this multigraph visits every terminal; shortcutting it — skip repeated and non-terminal vertices, keeping only the first visit to each terminal — gives a Hamiltonian cycle on the terminals of length (shortcutting never increases length, by the triangle inequality on shortest-path distances). Dropping the single most expensive edge of that cycle gives a spanning path, hence a spanning tree, on the terminals of cost — and the MST of the terminals’ metric closure is by definition no worse than any spanning tree on them, in particular this one. Finally, expanding each MST edge back into its shortest path gives an actual connected subgraph of the original graph spanning all terminals, of total cost equal to the terminal-MST’s cost (no further loss, since each edge’s weight in the metric closure already is the true shortest-path distance) — .
The best known ratio is (Byrka et al.), via LP rounding.
The variants
| Variant | Complexity |
|---|---|
| shortest path, P | |
| MST, P | |
| General | NP-hard |
| small | FPT: |
| Steiner tree in a tree | P — the answer is the virtual tree of the terminals |
| Steiner forest (pairs to connect) | NP-hard; 2-approximation by primal-dual |
| Directed Steiner tree (arborescence) | much harder; no constant-factor approximation known |
| Euclidean Steiner tree | NP-hard; PTAS exists; Steiner points meet at |
| Rectilinear Steiner tree | NP-hard; VLSI routing |
| Prize-collecting Steiner tree | NP-hard; 2-approximation |
| Node-weighted Steiner tree | -approximable |
Steiner tree in a tree
When the graph is itself a tree, the minimum connected subgraph containing the terminals is simply the union of the paths between them — computable in with a virtual tree:
Equivalently, sort the terminals by DFS entry time and sum around the cycle, halved:
sort(terms.begin(), terms.end(), byTin);
ll total = 0;
for (int i = 0; i < k; i++) total += dist(terms[i], terms[(i + 1) % k]);
total /= 2;Proof: the cyclic-sum identity
The minimal connected subtree spanning the terminals (the “Steiner tree in a tree”) is exactly their virtual tree: the union of all pairwise paths between terminals, which is also the union of root-to-terminal paths pruned of any part not on the way to at least one terminal. Consider the terminals sorted by DFS entry time (tin) and connected cyclically : this cyclic sequence of paths together traverses every edge of the virtual tree exactly twice — once “going down and across” into a subtree in DFS order and once “coming back” when a later terminal’s path returns through it, which happens because consecutive terminals in tin-order are exactly the ones whose connecting path in the virtual tree enters and leaves each branch in a single down-up sweep (the same reasoning as an Euler tour of a tree traversing each edge exactly twice). So counts every virtual-tree edge’s weight exactly twice, and halving gives the true cost.
Tested: the cyclic-sum formula checked against brute-force search over all subsets of tree edges (keep the minimum-weight edge subset connecting all terminals) on random trees with up to 15 vertices and up to 6 terminals.
Where it appears
- Network design — connect a set of sites at minimum cable cost.
- VLSI routing — the rectilinear version.
- Phylogenetics — minimum evolution trees.
- Contest problems — usually with terminals, signalling Dreyfus-Wagner.
The recognition cue
When a problem says “connect these special vertices at minimum cost” and is suspiciously small () while is large, it is a Steiner tree and the intended solution is the DP. That constraint pattern is the giveaway.
Variants / Use Cases
- Steiner tree in a grid / on a plane — the geometric version (Euclidean Steiner tree) is also NP-hard, with a different structure.
- Held-Karp — the same “DP over subsets of a small special set” pattern.
- Network design, VLSI routing, phylogenetics — where Steiner trees originate.