Reducibility
Glossary Contents
← Ch XV
Chapter XVI Steiner Tree
Ch XVII →
  1. 46 — The cheapest connection
  2. 47 — Metric closure in TypeScript
  3. 48 — Wires on a chip
47

Metric closure in TypeScript

The 2-approximation: build the metric closure on terminals, take its MST, expand back into the original graph.

  1. 01
    Define types for edges and graphs. TypeScript's structural typing means an "object literal that looks like a Graph" is a Graph — no class, no constructor.
    type Edge = { u: number; v: number; w: number };
    type Graph = { n: number; adj: Map<number, { to: number; w: number }[]> };
    
    function makeGraph(n: number, edges: Edge[]): Graph {
      const adj = new Map<number, { to: number; w: number }[]>();
      for (let i = 0; i < n; i++) adj.set(i, []);
      for (const e of edges) {
        adj.get(e.u)!.push({ to: e.v, w: e.w });
        adj.get(e.v)!.push({ to: e.u, w: e.w });
      }
      return { n, adj };
    }
  2. 02
    Dijkstra's all-pairs shortest paths from each terminal. The "metric closure" is the complete graph on terminals where each edge weight equals shortest-path distance in the original.
    function dijkstra(g: Graph, src: number): number[] {
      const dist = new Array(g.n).fill(Infinity);
      dist[src] = 0;
      const visited = new Set<number>();
      while (visited.size < g.n) {
        let u = -1, best = Infinity;
        for (let i = 0; i < g.n; i++)
          if (!visited.has(i) && dist[i] < best) { u = i; best = dist[i]; }
        if (u < 0) break;
        visited.add(u);
        for (const { to, w } of g.adj.get(u)!)
          if (dist[u] + w < dist[to]) dist[to] = dist[u] + w;
      }
      return dist;
    }
  3. 03
    MST on the metric closure (Kruskal with union-find). The result is a tree on terminals; each "metric-closure edge" expands back to a real shortest path in G.
    function steinerApprox(g: Graph, terminals: number[]): Edge[] {
      const dist = terminals.map(t => dijkstra(g, t));
      const closureEdges: Edge[] = [];
      for (let i = 0; i < terminals.length; i++)
        for (let j = i + 1; j < terminals.length; j++)
          closureEdges.push({ u: i, v: j, w: dist[i][terminals[j]] });
      closureEdges.sort((a, b) => a.w - b.w);
    
      const parent = terminals.map((_, i) => i);
      const find = (x: number): number => parent[x] === x ? x : (parent[x] = find(parent[x]));
      const tree: Edge[] = [];
      for (const e of closureEdges) {
        const ru = find(e.u), rv = find(e.v);
        if (ru !== rv) { parent[ru] = rv; tree.push(e); }
      }
      return tree;
    }
  4. 04
    Run on a five-node ring with three terminals. The 2-approximation gives a tree of weight at most twice the optimum, which on real-world inputs lands within ~10–20% of optimal.
    const g = makeGraph(5, [
      { u: 0, v: 1, w: 1 }, { u: 1, v: 2, w: 1 }, { u: 2, v: 3, w: 1 },
      { u: 3, v: 4, w: 1 }, { u: 4, v: 0, w: 1 }
    ]);
    console.log(steinerApprox(g, [0, 2, 4]));
Grammar — TypeScript

TypeScript is JavaScript with a structural type system. Types are erased at runtime, so the output is plain JS — but at compile time you get the safety net. Graph algorithms benefit because edge/vertex shapes get checked.

type T = { f: A; g: B }
type alias — name a record shape. Structurally typed: any object with these fields satisfies T.
function f(x: T): R
typed function. Parameters get types after their names; return type after the close paren.
Map<K, V>
built-in keyed collection. Use Map.set / .get / .has, not square brackets.
const x = ...;
immutable binding (the variable can't be reassigned). Use `let` for mutable; never `var` in modern code.
T[]
array of T. The literal `[1, 2, 3]` is `number[]`.
(x: T) => R
arrow function — anonymous fn syntax. Body can be a single expression or a block.
expr as T
type assertion — tell the compiler "trust me, this is a T." Use sparingly.
x!.method()
non-null assertion — assert that x isn't null/undefined here. Useful after a Map.get when you know the key exists.
Infinity
a real number value, not a special type. Useful as the initial "no path yet" distance.
Weighted adjacency lists as Map<vertex, Array<{to, w}>>. The structural type means an inline object literal that has the right fields is an Edge — no class needed.
type Edge = { to: number; w: number };
const adj = new Map<number, Edge[]>();

adj.set(0, [{ to: 1, w: 5 }, { to: 2, w: 3 }]);
adj.set(1, [{ to: 0, w: 5 }]);

for (const { to, w } of adj.get(0) ?? []) {
    console.log(`edge to ${to}, weight ${w}`);
}
TypeScript Playground — official, in-browser →

Scan to come back to page 47

← 46 The cheapest connection
2/3 · 47/63
48 Wires on a chip →