rust_igraph/algorithms/flow/dominator_tree.rs
1//! `dominator_tree` (ALGO-FL-030) — Lengauer-Tarjan dominator tree of a
2//! directed flowgraph.
3//!
4//! Counterpart of `igraph_dominator_tree` from
5//! `references/igraph/src/flow/st-cuts.c:434-609` (with helpers at
6//! lines 286-375: the `dbucket` linked-list, and the `LINK` /
7//! `COMPRESS` / `EVAL` path-compression primitives).
8//!
9//! A *flowgraph* is a directed graph with a distinguished root `r`
10//! such that every vertex is conceptually reachable from `r`; in
11//! practice we tolerate unreachable vertices and surface them in the
12//! `leftout` list. A vertex `v` *dominates* `w` if every path from `r`
13//! to `w` passes through `v`; the *immediate dominator* `idom(w)` is
14//! the dominator closest to `w`. The edges `{(idom(w), w) | w ≠ r}`
15//! form a directed tree rooted at `r`, the dominator tree.
16//!
17//! Reference: Thomas Lengauer & Robert E. Tarjan, *A fast algorithm
18//! for finding dominators in a flowgraph*, ACM TOPLAS 1(1):121-141,
19//! 1979. <https://doi.org/10.1145/357062.357071>
20//!
21//! Complexity: `O(|V| + |E| · α(|E|, |V|))` with path-compression
22//! `EVAL`, where `α` is the inverse Ackermann function — effectively
23//! linear in `|V| + |E|`.
24//!
25//! ## Differences from the C port
26//!
27//! * No `+1` sentinel scheme. Where the C code stores `x + 1` to use
28//! `0` as "unset", the Rust port uses signed `i32` with `-1` as the
29//! sentinel — the additional arithmetic and the +1/-1 dance in the
30//! C code are gone.
31//! * The `igraph_adjlist_t` predecessor/successor lookups become two
32//! `Vec<Vec<u32>>` built once up front from
33//! [`Graph::out_neighbors_vec`] / [`Graph::in_neighbors_vec`]; the
34//! filter step that drops unreachable predecessors mirrors
35//! `st-cuts.c:522-535`.
36//! * The DFS is inlined here (instead of delegating to
37//! `algorithms::traversal::dfs::dfs`) so we capture both the parent
38//! pointer and the DFS pre-order in a single pass.
39
40// `i32 <-> usize` and `u32 <-> i32` casts are pervasive because we use
41// signed sentinels (`-1` = root/unset, `-2` = unreachable). The
42// validation at the top of `dominator_tree` rejects any `vcount` that
43// would not round-trip through `i32`, so every cast in this module is
44// bounded by `i32::MAX`. `too_many_lines` is allowed for the main
45// Lengauer-Tarjan kernel which mirrors the upstream C source.
46#![allow(
47 clippy::cast_possible_truncation,
48 clippy::cast_possible_wrap,
49 clippy::cast_sign_loss,
50 clippy::needless_range_loop,
51 clippy::too_many_lines
52)]
53
54use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
55
56/// Traversal direction for [`dominator_tree`].
57///
58/// Mirrors the `IGRAPH_OUT` / `IGRAPH_IN` choice of igraph C's
59/// `igraph_dominator_tree`. `IGRAPH_ALL` is not a valid mode for the
60/// dominator-tree problem and is therefore excluded from this enum.
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub enum DominatorMode {
63 /// Follow out-edges from `root`. This is the standard control-flow
64 /// dominator tree: an edge `u → v` in the input means `u` precedes
65 /// `v` in the flow.
66 Out,
67 /// Treat in-edges as forward edges (i.e. reverse every edge before
68 /// computing). Used to compute the *post-dominator* tree of a
69 /// control-flow graph and to mirror the upstream `IGRAPH_IN` mode.
70 In,
71}
72
73/// Result of [`dominator_tree`].
74#[derive(Clone, Debug)]
75pub struct DominatorTree {
76 /// Immediate-dominator vector of length `vcount`.
77 ///
78 /// * `idom[root] == -1`,
79 /// * `idom[v] == -2` for every vertex `v` unreachable from `root`,
80 /// * `idom[v] == idom_vertex_id` otherwise (always `>= 0` and
81 /// `< vcount`).
82 pub idom: Vec<i32>,
83 /// Directed graph on the same vertex set as the input. Contains
84 /// exactly one edge per non-root reachable vertex; orientation is
85 /// dictated by `mode` (`Out` ⇒ `idom(w) → w`, `In` ⇒ `w → idom(w)`).
86 /// Vertices in [`Self::leftout`] appear as isolates.
87 pub tree: Graph,
88 /// Vertex ids unreachable from `root`, in ascending order. Empty
89 /// when the graph is a proper flowgraph (every vertex reachable
90 /// from `root`).
91 pub leftout: Vec<VertexId>,
92}
93
94/// Compute the Lengauer-Tarjan dominator tree of a directed flowgraph.
95///
96/// See [`DominatorTree`] for the result shape and [`DominatorMode`] for
97/// the direction selector.
98///
99/// # Arguments
100///
101/// * `graph` — directed input graph. Undirected graphs are rejected.
102/// * `root` — root vertex id; must satisfy `root < graph.vcount()`.
103/// * `mode` — `Out` for the classical control-flow dominator tree,
104/// `In` for the post-dominator tree (every edge reversed).
105///
106/// # Errors
107///
108/// * [`IgraphError::InvalidArgument`] when `graph` is undirected or
109/// when `graph.vcount()` exceeds `i32::MAX` (which would make the
110/// internal DFS-order index overflow).
111/// * [`IgraphError::VertexOutOfRange`] when `root` is not a valid
112/// vertex id.
113///
114/// [`IgraphError::InvalidArgument`]: crate::core::IgraphError::InvalidArgument
115/// [`IgraphError::VertexOutOfRange`]: crate::core::IgraphError::VertexOutOfRange
116///
117/// # Examples
118///
119/// ```
120/// use rust_igraph::{Graph, dominator_tree, DominatorMode};
121///
122/// // 0 → 1: the root dominates every other reachable vertex trivially.
123/// let mut g = Graph::new(2, true).unwrap();
124/// g.add_edge(0, 1).unwrap();
125/// let dt = dominator_tree(&g, 0, DominatorMode::Out).unwrap();
126/// assert_eq!(dt.idom, vec![-1, 0]);
127/// assert_eq!(dt.leftout, Vec::<u32>::new());
128/// assert_eq!(dt.tree.ecount(), 1);
129/// ```
130pub fn dominator_tree(
131 graph: &Graph,
132 root: VertexId,
133 mode: DominatorMode,
134) -> IgraphResult<DominatorTree> {
135 let n = graph.vcount();
136
137 if !graph.is_directed() {
138 return Err(IgraphError::InvalidArgument(
139 "dominator tree of an undirected graph requested".to_string(),
140 ));
141 }
142 if root >= n {
143 return Err(IgraphError::VertexOutOfRange { id: root, n });
144 }
145 // We use i32 sentinels (-1 = root / unset, -2 = unreachable) and store
146 // vertex ids in i32 throughout; reject any input whose vcount cannot
147 // round-trip through i32.
148 if i32::try_from(n).is_err() {
149 return Err(IgraphError::InvalidArgument(format!(
150 "dominator_tree: vcount {n} exceeds i32::MAX"
151 )));
152 }
153
154 let n_us = n as usize;
155
156 // Build adjacency lists. `succ` is the forward direction (DFS walks
157 // this); `pred` is the reverse (used in the bucket loop). For
158 // mode == In every edge is conceptually reversed.
159 let mut succ: Vec<Vec<VertexId>> = Vec::with_capacity(n_us);
160 let mut pred: Vec<Vec<VertexId>> = Vec::with_capacity(n_us);
161 for v in 0..n {
162 let (s, p) = match mode {
163 DominatorMode::Out => (graph.out_neighbors_vec(v)?, graph.in_neighbors_vec(v)?),
164 DominatorMode::In => (graph.in_neighbors_vec(v)?, graph.out_neighbors_vec(v)?),
165 };
166 succ.push(s);
167 pred.push(p);
168 }
169
170 // ----- Step 1: DFS from `root`, set `parent` + DFS-order -----
171 // parent[v] = -1 for root, -2 for unreachable, else the DFS parent
172 // vertex id (always >= 0).
173 let mut parent: Vec<i32> = vec![-2; n_us];
174 let mut order: Vec<VertexId> = Vec::with_capacity(n_us);
175 let mut visited = vec![false; n_us];
176
177 parent[root as usize] = -1;
178 visited[root as usize] = true;
179 order.push(root);
180
181 let mut stack: Vec<(VertexId, usize)> = Vec::with_capacity(n_us);
182 stack.push((root, 0));
183
184 while let Some(&(v, cursor)) = stack.last() {
185 let mut next_cursor = cursor;
186 let neis = &succ[v as usize];
187 let mut found: Option<VertexId> = None;
188 while next_cursor < neis.len() {
189 let u = neis[next_cursor];
190 next_cursor += 1;
191 if !visited[u as usize] {
192 found = Some(u);
193 break;
194 }
195 }
196 let top_idx = stack.len() - 1;
197 if let Some(u) = found {
198 stack[top_idx].1 = next_cursor;
199 visited[u as usize] = true;
200 // Safe because we rejected n > i32::MAX up front, and v < n.
201 parent[u as usize] = v as i32;
202 order.push(u);
203 stack.push((u, 0));
204 } else {
205 stack.pop();
206 }
207 }
208
209 // semi[v] = DFS number of v (0..component_size); -1 if unreachable.
210 // vertex[i] = vertex id with DFS number i; only the first
211 // `component_size` slots are valid.
212 let component_size = order.len();
213 let mut semi: Vec<i32> = vec![-1; n_us];
214 let mut vertex: Vec<i32> = vec![-1; component_size];
215 for (i, &v) in order.iter().enumerate() {
216 // Both bounded by n <= i32::MAX, checked above.
217 semi[v as usize] = i as i32;
218 vertex[i] = v as i32;
219 }
220
221 // leftout: vertices unreachable from root, in ascending order.
222 let mut leftout: Vec<VertexId> = Vec::with_capacity(n_us - component_size);
223 for v in 0..n {
224 if parent[v as usize] == -2 {
225 leftout.push(v);
226 }
227 }
228
229 // Drop predecessors that lie in the unreachable set — they cannot
230 // contribute to any dominator path from `root` (mirrors
231 // st-cuts.c:520-535).
232 for plist in pred.iter_mut().take(n_us) {
233 plist.retain(|&u| parent[u as usize] >= -1);
234 }
235
236 // ----- Steps 2 & 3: bucket-driven semi-dominator + LINK / EVAL -----
237 // ancestor[v] = LINK-forest parent (-1 if v is its own tree root).
238 // label[v] = candidate minimum-semi vertex along the path.
239 let mut ancestor: Vec<i32> = vec![-1; n_us];
240 let mut label: Vec<i32> = (0..n_us).map(|i| i as i32).collect();
241
242 // Per-vertex bucket: bucket_head[bid] = -1 (empty) or first elem;
243 // bucket_next[elem] = -1 (last) or next elem id.
244 let mut bucket_head: Vec<i32> = vec![-1; n_us];
245 let mut bucket_next: Vec<i32> = vec![-1; n_us];
246
247 // Working idom array (sentinel -2 = unset; root + unreachable stay
248 // -2 until the tail of the function patches them).
249 let mut idom: Vec<i32> = vec![-2; n_us];
250
251 for i in (1..component_size).rev() {
252 let w_i32 = vertex[i];
253 let w_us = w_i32 as usize;
254
255 // Compute semi-dominator of w via every reachable predecessor.
256 let preds_len = pred[w_us].len();
257 for j in 0..preds_len {
258 let v_pred = pred[w_us][j] as i32;
259 let u = dom_eval(v_pred, &mut ancestor, &mut label, &semi);
260 let su = semi[u as usize];
261 let sw = semi[w_us];
262 if su >= 0 && (sw < 0 || su < sw) {
263 semi[w_us] = su;
264 }
265 }
266
267 // Insert w into bucket[ vertex[semi(w)] ].
268 let semi_w = semi[w_us];
269 let bucket_owner = vertex[semi_w as usize] as usize;
270 bucket_next[w_us] = bucket_head[bucket_owner];
271 bucket_head[bucket_owner] = w_i32;
272
273 // LINK(parent[w], w) — ancestor[w] := parent[w].
274 let pw = parent[w_us];
275 ancestor[w_us] = pw;
276
277 // Drain bucket[parent[w]], finalising each entry's idom.
278 let pw_us = pw as usize;
279 while bucket_head[pw_us] != -1 {
280 let v_b = bucket_head[pw_us];
281 bucket_head[pw_us] = bucket_next[v_b as usize];
282 let u = dom_eval(v_b, &mut ancestor, &mut label, &semi);
283 if semi[u as usize] < semi[v_b as usize] {
284 idom[v_b as usize] = u;
285 } else {
286 idom[v_b as usize] = pw;
287 }
288 }
289 }
290
291 // ----- Step 4: forward fixup pass -----
292 for i in 1..component_size {
293 let w_us = vertex[i] as usize;
294 let chk = vertex[semi[w_us] as usize];
295 if idom[w_us] != chk {
296 idom[w_us] = idom[idom[w_us] as usize];
297 }
298 }
299 idom[root as usize] = -1;
300
301 // ----- Materialise the dominator tree -----
302 let mut tree = Graph::new(n, true)?;
303 for w in 0..n {
304 if w == root {
305 continue;
306 }
307 let d = idom[w as usize];
308 if d < 0 {
309 // Unreachable vertex: appears as an isolate in the tree.
310 continue;
311 }
312 match mode {
313 DominatorMode::Out => tree.add_edge(d as VertexId, w)?,
314 DominatorMode::In => tree.add_edge(w, d as VertexId)?,
315 }
316 }
317
318 Ok(DominatorTree {
319 idom,
320 tree,
321 leftout,
322 })
323}
324
325/// `EVAL` primitive: finds the vertex on the LINK-forest path from `v`
326/// to its tree root with the smallest `semi` value, performing path
327/// compression along the way.
328fn dom_eval(v: i32, ancestor: &mut [i32], label: &mut [i32], semi: &[i32]) -> i32 {
329 if ancestor[v as usize] == -1 {
330 v
331 } else {
332 dom_compress(v, ancestor, label, semi);
333 label[v as usize]
334 }
335}
336
337/// `COMPRESS` primitive: walks up the LINK-forest path from `v` to the
338/// tree root (exclusive), propagating the minimum-`semi` `label`
339/// downward and rewiring every visited `ancestor` to point directly at
340/// the tree root.
341fn dom_compress(v: i32, ancestor: &mut [i32], label: &mut [i32], semi: &[i32]) {
342 // Collect the path v -> parent(v) -> ... -> last vertex whose
343 // ancestor is still non-null. The tree root itself (ancestor == -1)
344 // is NOT pushed.
345 let mut path: Vec<i32> = Vec::new();
346 let mut w = v;
347 while ancestor[w as usize] != -1 {
348 path.push(w);
349 w = ancestor[w as usize];
350 }
351 // Process from the shallowest pushed (last) to the deepest (first),
352 // matching the LIFO order of the C stack.
353 let mut iter = path.into_iter().rev();
354 let Some(mut top) = iter.next() else {
355 return;
356 };
357 for pretop in iter {
358 if semi[label[top as usize] as usize] < semi[label[pretop as usize] as usize] {
359 label[pretop as usize] = label[top as usize];
360 }
361 ancestor[pretop as usize] = ancestor[top as usize];
362 top = pretop;
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369
370 #[test]
371 fn rejects_undirected_graph() {
372 let g = Graph::with_vertices(2);
373 let err = dominator_tree(&g, 0, DominatorMode::Out).expect_err("must reject undirected");
374 assert!(matches!(err, IgraphError::InvalidArgument(_)));
375 }
376
377 #[test]
378 fn rejects_out_of_range_root() {
379 let g = Graph::new(3, true).expect("directed graph");
380 let err = dominator_tree(&g, 5, DominatorMode::Out).expect_err("must reject bad root");
381 match err {
382 IgraphError::VertexOutOfRange { id, n } => {
383 assert_eq!(id, 5);
384 assert_eq!(n, 3);
385 }
386 other => panic!("unexpected error: {other:?}"),
387 }
388 }
389
390 #[test]
391 fn root_alone_in_directed_graph() {
392 let mut g = Graph::new(3, true).expect("directed graph");
393 // 0 has no outgoing edges; vertices 1 and 2 are unreachable.
394 g.add_edge(1, 2).expect("add edge");
395 let dt = dominator_tree(&g, 0, DominatorMode::Out).expect("compute");
396 assert_eq!(dt.idom, vec![-1, -2, -2]);
397 assert_eq!(dt.leftout, vec![1, 2]);
398 assert_eq!(dt.tree.ecount(), 0);
399 assert_eq!(dt.tree.vcount(), 3);
400 }
401
402 /// Classical 13-vertex Lengauer-Tarjan example (numbered 0..12 in
403 /// the order used by `references/igraph/tests/unit/igraph_dominator_tree.c:23-46`).
404 /// In this graph: idom values are taken directly from
405 /// `references/igraph/tests/unit/igraph_dominator_tree.c:54-58`
406 /// after stripping the +1 the reference vector uses.
407 #[test]
408 fn lengauer_tarjan_classical_13v() {
409 let edges = [
410 (0u32, 1u32),
411 (0, 2),
412 (0, 3),
413 (1, 4),
414 (2, 1),
415 (2, 4),
416 (2, 5),
417 (3, 6),
418 (3, 7),
419 (4, 12),
420 (5, 8),
421 (6, 9),
422 (7, 9),
423 (7, 10),
424 (8, 5),
425 (8, 11),
426 (9, 11),
427 (10, 9),
428 (11, 0),
429 (11, 9),
430 (12, 8),
431 ];
432 let mut g = Graph::new(13, true).expect("directed graph");
433 for (u, v) in edges {
434 g.add_edge(u, v).expect("add edge");
435 }
436 let dt = dominator_tree(&g, 0, DominatorMode::Out).expect("compute");
437 // Reference (C unit test, +1 stripped):
438 // idom = [-1, 0, 0, 0, 0, 0, 3, 3, 0, 0, 7, 0, 4]
439 assert_eq!(dt.idom, vec![-1, 0, 0, 0, 0, 0, 3, 3, 0, 0, 7, 0, 4]);
440 assert!(dt.leftout.is_empty());
441 // Tree has |V| - 1 edges (every non-root reachable vertex
442 // contributes one).
443 assert_eq!(dt.tree.ecount(), 12);
444 }
445
446 /// Reversed-edge variant of the classical 13-vertex example with
447 /// `mode = In`: idom values match
448 /// `references/igraph/tests/unit/igraph_dominator_tree.c:80-91`.
449 #[test]
450 fn lengauer_tarjan_in_mode_matches_reverse_out_mode() {
451 // Build the *reverse* of the classical graph and run with OUT,
452 // then build the classical graph and run with IN. Both must
453 // produce the same idom (post-dominator tree of the classical
454 // example).
455 let edges = [
456 (0u32, 1u32),
457 (0, 2),
458 (0, 3),
459 (1, 4),
460 (2, 1),
461 (2, 4),
462 (2, 5),
463 (3, 6),
464 (3, 7),
465 (4, 12),
466 (5, 8),
467 (6, 9),
468 (7, 9),
469 (7, 10),
470 (8, 5),
471 (8, 11),
472 (9, 11),
473 (10, 9),
474 (11, 0),
475 (11, 9),
476 (12, 8),
477 ];
478 let mut g_forward = Graph::new(13, true).expect("forward");
479 for (u, v) in edges {
480 g_forward.add_edge(u, v).expect("add edge");
481 }
482 let mut g_reverse = Graph::new(13, true).expect("reverse");
483 for (u, v) in edges {
484 g_reverse.add_edge(v, u).expect("add reverse edge");
485 }
486
487 let in_mode = dominator_tree(&g_forward, 0, DominatorMode::In).expect("In mode");
488 let out_mode_reversed =
489 dominator_tree(&g_reverse, 0, DominatorMode::Out).expect("Out on reversed");
490
491 assert_eq!(in_mode.idom, out_mode_reversed.idom);
492 }
493
494 #[test]
495 fn unreachable_vertices_land_in_leftout() {
496 // Same shape as the 20-vertex test in
497 // references/igraph/tests/unit/igraph_dominator_tree.c:104-117:
498 // a tiny 4-vertex flowgraph plus a disconnected K3 cluster.
499 let mut g = Graph::new(7, true).expect("directed graph");
500 g.add_edge(0, 1).expect("e");
501 g.add_edge(0, 2).expect("e");
502 g.add_edge(1, 3).expect("e");
503 g.add_edge(2, 3).expect("e");
504 // Disconnected cluster: 4 → 5 → 6 → 4.
505 g.add_edge(4, 5).expect("e");
506 g.add_edge(5, 6).expect("e");
507 g.add_edge(6, 4).expect("e");
508
509 let dt = dominator_tree(&g, 0, DominatorMode::Out).expect("compute");
510 // 0 is the root, 1/2/3 reachable via 0, 4/5/6 unreachable.
511 assert_eq!(dt.idom[0], -1);
512 assert_eq!(dt.idom[1], 0);
513 assert_eq!(dt.idom[2], 0);
514 assert_eq!(dt.idom[3], 0);
515 assert_eq!(dt.idom[4], -2);
516 assert_eq!(dt.idom[5], -2);
517 assert_eq!(dt.idom[6], -2);
518 assert_eq!(dt.leftout, vec![4, 5, 6]);
519 assert_eq!(dt.tree.ecount(), 3);
520 }
521
522 #[test]
523 fn idom_lies_on_every_root_to_w_path_brute_force() {
524 // Small directed graph; brute-force verify the dominator
525 // property: for each w != root, every root→w path must pass
526 // through idom(w).
527 let mut g = Graph::new(6, true).expect("directed graph");
528 // A diamond with a side spur.
529 g.add_edge(0, 1).expect("e");
530 g.add_edge(0, 2).expect("e");
531 g.add_edge(1, 3).expect("e");
532 g.add_edge(2, 3).expect("e");
533 g.add_edge(3, 4).expect("e");
534 g.add_edge(4, 5).expect("e");
535 let dt = dominator_tree(&g, 0, DominatorMode::Out).expect("compute");
536 // Predictable shape: 0 dominates {1,2,3,4,5}; 3 dominates {4,5};
537 // 4 dominates {5}.
538 assert_eq!(dt.idom, vec![-1, 0, 0, 0, 3, 4]);
539
540 // Verify with simple DFS path enumeration (n is tiny).
541 for w in 1..6u32 {
542 let d = dt.idom[w as usize];
543 assert!(d >= 0, "vertex {w} should be reachable");
544 let d_u = d as u32;
545 let paths = enumerate_simple_paths(&g, 0, w);
546 assert!(!paths.is_empty(), "no path 0 -> {w}?");
547 for p in &paths {
548 if w != 0 {
549 assert!(
550 p.contains(&d_u),
551 "idom({w}) = {d_u} must appear on every path: {p:?}"
552 );
553 }
554 }
555 }
556 }
557
558 fn enumerate_simple_paths(g: &Graph, s: VertexId, t: VertexId) -> Vec<Vec<VertexId>> {
559 let mut out: Vec<Vec<VertexId>> = Vec::new();
560 let mut stack: Vec<VertexId> = vec![s];
561 let mut on_stack = vec![false; g.vcount() as usize];
562 on_stack[s as usize] = true;
563 dfs_paths(g, s, t, &mut stack, &mut on_stack, &mut out);
564 out
565 }
566
567 fn dfs_paths(
568 g: &Graph,
569 cur: VertexId,
570 t: VertexId,
571 stack: &mut Vec<VertexId>,
572 on_stack: &mut [bool],
573 out: &mut Vec<Vec<VertexId>>,
574 ) {
575 if cur == t {
576 out.push(stack.clone());
577 return;
578 }
579 let neis = g.out_neighbors_vec(cur).expect("out neis");
580 for u in neis {
581 if !on_stack[u as usize] {
582 on_stack[u as usize] = true;
583 stack.push(u);
584 dfs_paths(g, u, t, stack, on_stack, out);
585 stack.pop();
586 on_stack[u as usize] = false;
587 }
588 }
589 }
590}