Skip to main content

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}