Dominator Tree (Lengauer-Tarjan) — Practice Tasks¶
All tasks must be solved in Go, Java, and Python. Each task ships with a problem statement, constraints, hints, and reference starter/solution code in all three languages. Recall the pipeline: DFS numbering → semidominators → link-eval with path compression → correct
sdomintoidom, with the iterative dataflow fixpoint as the simple oracle.
Beginner Tasks (5)¶
Task 1 — Dominators by brute force (definition oracle)¶
Problem. For a small flow graph (entry r), compute idom[v] for every vertex by directly applying the definition: u dominates v iff removing u makes v unreachable from r. This is your correctness oracle for all later tasks.
Constraints. 1 ≤ n ≤ 30. Vertices 0..n-1, all reachable from r. Time O(n·(n+m)) (one reachability check per removed vertex).
Hint. For each vertex u, run a BFS/DFS from r with u deleted; any v that becomes unreachable is dominated by u. Then idom(v) is the strict dominator of v with the largest dominator set.
Go¶
package main
import "fmt"
func bruteIdom(n int, succ [][]int, r int) []int {
reach := func(block int) map[int]bool {
seen := map[int]bool{}
if r == block {
return seen
}
stack := []int{r}
seen[r] = true
for len(stack) > 0 {
u := stack[len(stack)-1]
stack = stack[:len(stack)-1]
for _, v := range succ[u] {
if v != block && !seen[v] {
seen[v] = true
stack = append(stack, v)
}
}
}
return seen
}
all := reach(-1)
dom := make([]map[int]bool, n)
for v := range dom {
dom[v] = map[int]bool{}
}
for u := 0; u < n; u++ {
without := reach(u)
for v := range all {
if v != u && !without[v] {
dom[v][u] = true
}
}
}
idom := make([]int, n)
for i := range idom {
idom[i] = -1
}
idom[r] = r
for v := range all {
if v == r {
continue
}
dom[v][v] = true
best, bestSize := -1, -1
for u := range dom[v] {
if u != v && len(dom[u]) > bestSize {
bestSize = len(dom[u])
best = u
}
}
idom[v] = best
}
return idom
}
func main() {
succ := [][]int{{1, 2}, {3}, {3}, {4, 5}, {5}, {}}
fmt.Println(bruteIdom(6, succ, 0)) // [0 0 0 0 3 3]
}
Java¶
import java.util.*;
public class Task1 {
static int n, r;
static List<List<Integer>> succ;
static Set<Integer> reach(int block) {
Set<Integer> seen = new HashSet<>();
if (r == block) return seen;
Deque<Integer> st = new ArrayDeque<>(); st.push(r); seen.add(r);
while (!st.isEmpty()) {
int u = st.pop();
for (int v : succ.get(u))
if (v != block && seen.add(v)) st.push(v);
}
return seen;
}
static int[] bruteIdom() {
Set<Integer> all = reach(-1);
List<Set<Integer>> dom = new ArrayList<>();
for (int i = 0; i < n; i++) dom.add(new HashSet<>());
for (int u = 0; u < n; u++) {
Set<Integer> without = reach(u);
for (int v : all) if (v != u && !without.contains(v)) dom.get(v).add(u);
}
int[] idom = new int[n]; Arrays.fill(idom, -1); idom[r] = r;
for (int v : all) {
if (v == r) continue;
dom.get(v).add(v);
int best = -1, bestSize = -1;
for (int u : dom.get(v))
if (u != v && dom.get(u).size() > bestSize) { bestSize = dom.get(u).size(); best = u; }
idom[v] = best;
}
return idom;
}
public static void main(String[] args) {
n = 6; r = 0;
succ = List.of(List.of(1,2), List.of(3), List.of(3), List.of(4,5), List.of(5), List.of());
System.out.println(Arrays.toString(bruteIdom())); // [0, 0, 0, 0, 3, 3]
}
}
Python¶
def brute_idom(n, succ, r):
def reach(block):
if r == block:
return set()
seen, stack = {r}, [r]
while stack:
u = stack.pop()
for v in succ[u]:
if v != block and v not in seen:
seen.add(v)
stack.append(v)
return seen
all_ = reach(-1)
dom = [set() for _ in range(n)]
for u in range(n):
without = reach(u)
for v in all_:
if v != u and v not in without:
dom[v].add(u)
idom = [-1] * n
idom[r] = r
for v in all_:
if v == r:
continue
dom[v].add(v)
strict = [u for u in dom[v] if u != v]
idom[v] = max(strict, key=lambda u: len(dom[u]))
return idom
if __name__ == "__main__":
succ = [[1, 2], [3], [3], [4, 5], [5], []]
print(brute_idom(6, succ, 0)) # [0, 0, 0, 0, 3, 3]
- Evaluation: correctness vs hand-computed
idom, edge cases (single vertex, chain).
Task 2 — Build the dominator tree from an idom array¶
Problem. Given idom[] (with idom[r] = r), produce children[]: for each vertex, the list of vertices whose immediate dominator it is. Print the tree as parent→children lines.
Constraints. 1 ≤ n ≤ 10⁵. O(n) time and space.
Hint. For every v ≠ r with idom[v] != -1, append v to children[idom[v]]. Skip the root self-edge.
Provide starter code that reads idom and prints children in all three languages (Go slice append, Java List, Python list-of-lists).
- Constraints: do not add
ras its own child; handleidom[v] == -1(unreachable) by skipping.
Task 3 — Detect a back edge given the dominator tree¶
Problem. Given a CFG and its idom[], identify all back edges: edges t → h where h dominates t. These are the loop-defining edges.
Constraints. 1 ≤ n ≤ 10⁵. Preprocess for O(1) dominance, then scan edges in O(E).
Hint. Euler-tour the dominator tree for in/out times; h dominates t iff tin[h] ≤ tin[t] && tout[t] ≤ tout[h]. An edge t → h is a back edge iff h dominates t.
Provide starter code that computes in/out times via an explicit-stack DFS of the dominator tree and tests each edge, in all three languages.
- Expected Output: for the running example plus edge
5 → 3, the back edge is(5, 3)with header3.
Task 4 — Print the dominator-tree depth of every vertex¶
Problem. Given idom[], compute depth[v] = number of edges from r to v in the dominator tree (depth[r] = 0).
Constraints. 1 ≤ n ≤ 10⁵. O(n) via a BFS/DFS of the dominator tree, or memoized walk up idom.
Hint. depth[v] = depth[idom[v]] + 1 for v ≠ r. Compute in dominator-tree preorder, or memoize a walk up the idom chain.
Provide starter code in all three languages (recursive-with-memo or explicit BFS from r over children[]).
- Evaluation: correct depths, no recomputation (each vertex resolved once).
Task 5 — Nearest common dominator of two vertices (naive)¶
Problem. Given idom[] and queries (a, b), return the nearest common dominator — the lowest vertex in the dominator tree that dominates both a and b (their LCA in the dom tree).
Constraints. 1 ≤ n ≤ 10⁴, up to 10⁴ queries. Naive walk is fine here: O(depth) per query.
Hint. Use the two-finger intersect from the iterative dataflow algorithm: bring the deeper vertex up by idom until depths match, then advance both until they meet. Use the depth[] from Task 4.
Go¶
package main
import "fmt"
func ncd(a, b int, idom, depth []int) int {
for depth[a] > depth[b] {
a = idom[a]
}
for depth[b] > depth[a] {
b = idom[b]
}
for a != b {
a = idom[a]
b = idom[b]
}
return a
}
func main() {
idom := []int{0, 0, 0, 0, 3, 3}
depth := []int{0, 1, 1, 1, 2, 2}
fmt.Println(ncd(4, 5, idom, depth)) // 3
fmt.Println(ncd(1, 5, idom, depth)) // 0
}
Java¶
public class Task5 {
static int ncd(int a, int b, int[] idom, int[] depth) {
while (depth[a] > depth[b]) a = idom[a];
while (depth[b] > depth[a]) b = idom[b];
while (a != b) { a = idom[a]; b = idom[b]; }
return a;
}
public static void main(String[] args) {
int[] idom = {0, 0, 0, 0, 3, 3};
int[] depth = {0, 1, 1, 1, 2, 2};
System.out.println(ncd(4, 5, idom, depth)); // 3
System.out.println(ncd(1, 5, idom, depth)); // 0
}
}
Python¶
def ncd(a, b, idom, depth):
while depth[a] > depth[b]:
a = idom[a]
while depth[b] > depth[a]:
b = idom[b]
while a != b:
a = idom[a]
b = idom[b]
return a
if __name__ == "__main__":
idom = [0, 0, 0, 0, 3, 3]
depth = [0, 1, 1, 1, 2, 2]
print(ncd(4, 5, idom, depth)) # 3
print(ncd(1, 5, idom, depth)) # 0
- Evaluation: correctness against brute LCA; terminate when
a == b.
Intermediate Tasks (5)¶
Task 6 — Iterative dataflow dominators (Cooper-Harvey-Kennedy)¶
Problem. Implement the full iterative dataflow idom algorithm: reverse-postorder ordering, the intersect two-finger walk on RPO numbers, and the fixpoint loop. Return idom[].
Constraints. 1 ≤ n ≤ 10⁵. Process in reverse postorder; converge in a few sweeps for reducible graphs.
Hint. Compute a DFS postorder, reverse it for RPO, assign rpoNum. Initialize idom[r] = r, others -1. Repeatedly, for each v ≠ r in RPO, fold its processed predecessors with intersect. Mark changed only on an actual update.
Provide starter code in all three languages (the full implementation is in junior.md's "Code Examples"; reproduce and test it here against the Task 1 oracle).
- Evaluation: matches the brute oracle; terminates; uses an explicit-stack DFS for postorder.
Task 7 — Lengauer-Tarjan (simple, path compression)¶
Problem. Implement the simple O(E log V) Lengauer-Tarjan with DFS numbering, semidominators, and EVAL/LINK with path compression. Return idom[].
Constraints. 1 ≤ n ≤ 2·10⁵, 1 ≤ m ≤ 5·10⁵. Use an explicit-stack DFS for large/deep graphs.
Hint. Follow the four phases. The bucket of sdom(w) is processed when you finish w and link it; finalize deferred idom in a forward pass: if idom[w] != vertex[semi[w]]: idom[w] = idom[idom[w]].
Provide starter code in all three languages (the full implementation is in middle.md and interview.md Challenge 1; reproduce it and differential-test against Task 6).
- Evaluation: matches iterative dataflow on 1000+ random CFGs; handles a diamond and a nested loop.
Task 8 — Dominance frontiers¶
Problem. Given a CFG and its idom[], compute the dominance frontier DF(u) for every vertex u — the set of merge blocks where u's dominance ends. Output DF as adjacency sets.
Constraints. 1 ≤ n ≤ 10⁵. Use the Cooper-Harvey-Kennedy edge-walk method.
Hint. For each CFG edge (a, b) where b has ≥ 2 predecessors, walk runner = a up idom until runner == idom[b], adding b to DF(runner) each step.
Go¶
package main
import "fmt"
func dominanceFrontiers(n int, preds [][]int, idom []int) [][]int {
df := make([]map[int]bool, n)
for i := range df {
df[i] = map[int]bool{}
}
for b := 0; b < n; b++ {
if len(preds[b]) < 2 {
continue
}
for _, p := range preds[b] {
runner := p
for runner != idom[b] && runner != -1 {
df[runner][b] = true
runner = idom[runner]
}
}
}
out := make([][]int, n)
for u := 0; u < n; u++ {
for w := range df[u] {
out[u] = append(out[u], w)
}
}
return out
}
func main() {
preds := [][]int{{}, {0}, {0}, {1, 2}, {3}, {3, 4}}
idom := []int{0, 0, 0, 0, 3, 3}
df := dominanceFrontiers(6, preds, idom)
for u := 0; u < 6; u++ {
fmt.Printf("DF(%d) = %v\n", u, df[u])
}
}
Java¶
import java.util.*;
public class Task8 {
static List<Set<Integer>> dominanceFrontiers(int n, List<List<Integer>> preds, int[] idom) {
List<Set<Integer>> df = new ArrayList<>();
for (int i = 0; i < n; i++) df.add(new HashSet<>());
for (int b = 0; b < n; b++) {
if (preds.get(b).size() < 2) continue;
for (int p : preds.get(b)) {
int runner = p;
while (runner != idom[b] && runner != -1) {
df.get(runner).add(b);
runner = idom[runner];
}
}
}
return df;
}
public static void main(String[] args) {
List<List<Integer>> preds = List.of(
List.of(), List.of(0), List.of(0), List.of(1,2), List.of(3), List.of(3,4));
int[] idom = {0, 0, 0, 0, 3, 3};
List<Set<Integer>> df = dominanceFrontiers(6, preds, idom);
for (int u = 0; u < 6; u++) System.out.println("DF(" + u + ") = " + df.get(u));
}
}
Python¶
def dominance_frontiers(n, preds, idom):
df = [set() for _ in range(n)]
for b in range(n):
if len(preds[b]) < 2:
continue
for p in preds[b]:
runner = p
while runner != idom[b] and runner != -1:
df[runner].add(b)
runner = idom[runner]
return df
if __name__ == "__main__":
preds = [[], [0], [0], [1, 2], [3], [3, 4]]
idom = [0, 0, 0, 0, 3, 3]
df = dominance_frontiers(6, preds, idom)
for u in range(6):
print(f"DF({u}) = {sorted(df[u])}")
- Evaluation:
DF(1) = {3},DF(2) = {3},DF(4) = {5}on this CFG.
Task 9 — Natural loop bodies¶
Problem. Given a CFG and idom[], find every natural loop: for each back edge t → h, return h (header) and the set of body blocks (h, t, and everything reaching t without going through h).
Constraints. 1 ≤ n ≤ 10⁵. Detect back edges via dominance (Task 3), then backward-flood from t stopping at h.
Hint. Body = {h}; start a worklist with t; for each popped block, add unvisited predecessors (never re-expand h). Merge bodies sharing a header.
Provide starter code in all three languages (the Python version is in senior.md §7.3; port it to Go and Java).
- Evaluation: on the running example plus
5 → 3, the loop has header3and body{3, 4, 5}.
Task 10 — Post-dominators via graph reversal¶
Problem. Compute the post-dominator of every vertex: run your dominator algorithm on the reversed CFG with the exit as root. Add a synthetic exit if there are multiple sinks.
Constraints. 1 ≤ n ≤ 10⁵. Reuse the Task 7 (or Task 6) dominator code unchanged on reversed edges.
Hint. Build rev[v] = [u for each edge u→v]. If multiple sinks (vertices with no successors), add a synthetic node n with an edge from each sink, and run with root n.
Provide starter code in all three languages that reverses edges, adds a synthetic exit if needed, and calls the dominator routine.
- Evaluation: post-
idomis exit-rooted; verify a node's post-dominator is the next unavoidable block on the way to exit.
Advanced Tasks (5)¶
Task 11 — Differential tester (fast vs oracle)¶
Problem. Build a randomized differential tester: generate random reachable CFGs, compute idom with both the Task 1 brute oracle and the Task 7 Lengauer-Tarjan, and assert they agree. Report any mismatch with the failing graph.
Constraints. Generate 1000+ graphs with 2 ≤ n ≤ 12 (oracle is exponential-ish, keep n small). Ensure reachability via a random spanning tree plus extra edges.
Hint. Random spanning tree: for v in 1..n-1, add edge from random(0..v-1) to v. Then sprinkle extra random edges. On mismatch, print n, the edge list, and both idom arrays.
Provide starter code in all three languages (the Python skeleton is in professional.md §12.3).
- Evaluation: runs clean on a correct implementation; deliberately break
EVAL(drop path compression) and confirm the tester catches a mismatch.
Task 12 — Lengauer-Tarjan with balanced linking (O(E·α(V)))¶
Problem. Upgrade the simple Lengauer-Tarjan to the sophisticated version: add size-based balanced linking in LINK so the amortized bound improves from O(E log V) to O(E·α(V)).
Constraints. 1 ≤ n ≤ 5·10⁵, 1 ≤ m ≤ 2·10⁶. Must still match the simple version on differential tests.
Hint. Maintain size[], child[], and re-rooted label[]; in LINK(v, w), attach the smaller tree under the larger, walking and relabeling along the spine. Follow Lengauer-Tarjan (1979) §4 "sophisticated EVAL/LINK."
Provide starter code in all three languages with the sophisticated LINK/EVAL; benchmark against Task 7 on a large graph.
- Evaluation: identical
idomto Task 7; measurably better scaling on deep/wide adversarial graphs.
Task 13 — Iterated dominance frontier and φ-placement¶
Problem. Given a CFG, idom[], and a set of definition blocks D_x for a variable x, compute the iterated dominance frontier DF⁺(D_x ∪ {r}) — the exact set of blocks needing a φ-function for x.
Constraints. 1 ≤ n ≤ 10⁵. Use a worklist over DF (Task 8).
Hint. Worklist = D_x. Pop b; for each w ∈ DF(b) not yet a φ-site, mark it and add w to the worklist (its φ is a new definition). Repeat to fixpoint.
Provide starter code in all three languages that takes DF (from Task 8) and D_x, and returns the φ-placement set.
- Evaluation: correct minimal φ sites; matches a brute-force reaching-definitions placement on small CFGs.
Task 14 — Strong articulation points (directed 2-connectivity)¶
Problem. In a strongly connected directed graph, a vertex is a strong articulation point if removing it makes the graph not strongly connected. Compute all of them using dominator trees of G (rooted at any s) and of the reverse graph (rooted at s).
Constraints. 1 ≤ n ≤ 10⁵, 1 ≤ m ≤ 5·10⁵. Linear-time via the Italiano-Georgiadis result.
Hint. A vertex v ≠ s is a strong articulation point iff it is a non-trivial dominator (idom[w] = v for some w) in the dominator tree of G or of the reverse graph; s itself is an SAP iff it has ≥ 2 dominator-tree children in either. Combine both trees' non-trivial dominators.
Provide starter code in all three languages that runs the dominator algorithm twice (forward and reversed) and unions the non-trivial dominators.
- Evaluation: matches a brute force that removes each vertex and re-tests strong connectivity on small graphs.
Task 15 — Incremental dominator update on edge insertion¶
Problem. Maintain idom[] under a stream of edge insertions without full rebuilds. When inserting (u, v), update only the idoms that can change (which can only move up toward the root).
Constraints. 1 ≤ n ≤ 10⁴, up to 10⁴ insertions. Each update should touch far less than the whole tree on average.
Hint. If u is unreachable, ignore until reachable. Otherwise the new edge can lower idom[v] (and descendants) to ncd(idom[v], u). Recompute affected idoms by taking nearest common dominators along the changed subtree; verify against a full rebuild after each insertion.
Provide starter code in all three languages: start from a base idom, apply one insertion with the ncd-based patch, and assert equality with a full Lengauer-Tarjan rebuild.
- Evaluation: patched
idomequals a from-scratch rebuild after every insertion; average work per insertion well belowO(n)on random sequences.
Benchmark Task¶
Compare dominator computation across all 3 languages and both algorithms (iterative dataflow vs Lengauer-Tarjan).
Go¶
package main
import (
"fmt"
"math/rand"
"time"
)
func randomCFG(n int) [][2]int {
var edges [][2]int
for v := 1; v < n; v++ {
edges = append(edges, [2]int{rand.Intn(v), v}) // spanning tree -> reachable
}
for i := 0; i < n; i++ {
u, v := rand.Intn(n), rand.Intn(n-1)+1
if u != v {
edges = append(edges, [2]int{u, v})
}
}
return edges
}
func main() {
for _, n := range []int{1000, 10000, 100000} {
edges := randomCFG(n)
start := time.Now()
_ = computeIdom(n, edges, 0) // Lengauer-Tarjan from interview.md Challenge 1
fmt.Printf("n=%7d: %.2f ms\n", n, float64(time.Since(start).Microseconds())/1000.0)
}
}
Java¶
import java.util.*;
public class Benchmark {
public static void main(String[] args) {
Random rng = new Random(1);
for (int n : new int[]{1000, 10000, 100000}) {
List<int[]> edges = new ArrayList<>();
for (int v = 1; v < n; v++) edges.add(new int[]{rng.nextInt(v), v});
for (int i = 0; i < n; i++) {
int u = rng.nextInt(n), v = rng.nextInt(n - 1) + 1;
if (u != v) edges.add(new int[]{u, v});
}
int[][] arr = edges.toArray(new int[0][]);
long start = System.nanoTime();
new DominatorTree().computeIdom(n, arr, 0); // interview.md Challenge 1
System.out.printf("n=%7d: %.2f ms%n", n, (System.nanoTime() - start) / 1_000_000.0);
}
}
}
Python¶
import random
import time
# from interview.md Challenge 1: compute_idom(n, edges, r)
def random_cfg(n):
edges = [(random.randint(0, v - 1), v) for v in range(1, n)]
for _ in range(n):
u, v = random.randint(0, n - 1), random.randint(1, n - 1)
if u != v:
edges.append((u, v))
return edges
if __name__ == "__main__":
for n in (1000, 10_000, 100_000):
edges = random_cfg(n)
start = time.perf_counter()
compute_idom(n, edges, 0)
print(f"n={n:>7}: {(time.perf_counter() - start) * 1000:.2f} ms")