Skip to main content

oxicuda_graphalg/shortest_path/
transitive_closure.rs

1//! Transitive closure of a directed graph.
2//!
3//! The transitive closure of a digraph `G` is the relation `R` where `R[u][v]`
4//! holds iff there is a directed path from `u` to `v` in `G`. With
5//! `reflexive = true` the diagonal `R[v][v]` is forced true regardless of paths;
6//! with `reflexive = false` the diagonal is true only when `v` lies on a cycle
7//! (so a path of length `>= 1` returns to it).
8//!
9//! Two independent constructions are provided so they can cross-check:
10//!   * [`transitive_closure`] — boolean Floyd-Warshall, `O(V^3)`.
11//!   * [`transitive_closure_bfs`] — one BFS per source, `O(V * (V + E))`.
12
13use std::collections::VecDeque;
14
15use crate::error::{GraphalgError, GraphalgResult};
16use crate::repr::adjacency_list::AdjacencyList;
17
18/// Dense boolean reachability relation for an `n`-vertex digraph.
19///
20/// The matrix is stored row-major: entry `(u, v)` lives at index `u * n + v`.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct TransitiveClosure {
23    /// Number of vertices.
24    pub n: usize,
25    /// Row-major `n * n` reachability bits.
26    reach: Vec<bool>,
27    /// Whether the closure was computed with the reflexive diagonal forced on.
28    reflexive: bool,
29}
30
31impl TransitiveClosure {
32    /// Is `v` reachable from `u`? Returns [`GraphalgError::IndexOutOfBounds`] if
33    /// either index is `>= n`.
34    pub fn reachable(&self, u: usize, v: usize) -> GraphalgResult<bool> {
35        if u >= self.n {
36            return Err(GraphalgError::IndexOutOfBounds {
37                index: u,
38                len: self.n,
39            });
40        }
41        if v >= self.n {
42            return Err(GraphalgError::IndexOutOfBounds {
43                index: v,
44                len: self.n,
45            });
46        }
47        Ok(self.reach[u * self.n + v])
48    }
49
50    /// Borrow the underlying row-major reachability matrix (`n * n` bits).
51    pub fn matrix(&self) -> &[bool] {
52        &self.reach
53    }
54
55    /// Whether this closure was built with the reflexive diagonal forced on.
56    pub fn is_reflexive(&self) -> bool {
57        self.reflexive
58    }
59}
60
61/// Transitive closure via boolean Floyd-Warshall, `O(V^3)`.
62///
63/// `R[i][j]` is initialised to `edge(i -> j)` (plus the diagonal when
64/// `reflexive`), then for every intermediate `k` it is updated by
65/// `R[i][j] |= R[i][k] & R[k][j]`.
66pub fn transitive_closure(
67    graph: &AdjacencyList,
68    reflexive: bool,
69) -> GraphalgResult<TransitiveClosure> {
70    let n = graph.n;
71    let mut reach = vec![false; n * n];
72
73    // Seed with direct edges.
74    for u in 0..n {
75        for &v in graph.neighbors(u)? {
76            reach[u * n + v] = true;
77        }
78    }
79    if reflexive {
80        for v in 0..n {
81            reach[v * n + v] = true;
82        }
83    }
84
85    // Floyd-Warshall closure: for each pivot k, connect i->k->j.
86    for k in 0..n {
87        // Splitting the borrow: row k is read-only inside the i-loop.
88        for i in 0..n {
89            if reach[i * n + k] {
90                let base_i = i * n;
91                let base_k = k * n;
92                for j in 0..n {
93                    if reach[base_k + j] {
94                        reach[base_i + j] = true;
95                    }
96                }
97            }
98        }
99    }
100
101    Ok(TransitiveClosure {
102        n,
103        reach,
104        reflexive,
105    })
106}
107
108/// Transitive closure via one breadth-first search per source,
109/// `O(V * (V + E))`.
110///
111/// From each source `s`, BFS marks every vertex reachable through a path of
112/// length `>= 1`; those bits form row `s`. When `reflexive` is set the diagonal
113/// is forced true afterwards (otherwise `R[s][s]` is true only if a cycle
114/// brings the search back to `s`).
115pub fn transitive_closure_bfs(
116    graph: &AdjacencyList,
117    reflexive: bool,
118) -> GraphalgResult<TransitiveClosure> {
119    let n = graph.n;
120    let mut reach = vec![false; n * n];
121    let mut visited = vec![false; n];
122    let mut queue: VecDeque<usize> = VecDeque::new();
123
124    for s in 0..n {
125        // Reset the per-source scratch state.
126        for v in visited.iter_mut() {
127            *v = false;
128        }
129        queue.clear();
130
131        // Seed the queue with the direct successors of s (length-1 reach). We do
132        // NOT mark s as visited up front, so a cycle returning to s correctly
133        // sets R[s][s] even without the reflexive flag.
134        for &v in graph.neighbors(s)? {
135            if !visited[v] {
136                visited[v] = true;
137                reach[s * n + v] = true;
138                queue.push_back(v);
139            }
140        }
141        while let Some(u) = queue.pop_front() {
142            for &w in graph.neighbors(u)? {
143                if !visited[w] {
144                    visited[w] = true;
145                    reach[s * n + w] = true;
146                    queue.push_back(w);
147                }
148            }
149        }
150
151        if reflexive {
152            reach[s * n + s] = true;
153        }
154    }
155
156    Ok(TransitiveClosure {
157        n,
158        reach,
159        reflexive,
160    })
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    /// Directed chain 0 -> 1 -> 2 -> 3.
168    fn chain(n: usize) -> AdjacencyList {
169        let mut g = AdjacencyList::new(n);
170        for i in 0..n.saturating_sub(1) {
171            g.add_edge(i, i + 1).expect("edge");
172        }
173        g
174    }
175
176    #[test]
177    fn chain_upper_triangular_irreflexive() {
178        let g = chain(4);
179        let tc = transitive_closure(&g, false).expect("tc");
180        for i in 0..4 {
181            for j in 0..4 {
182                let expected = i < j; // strictly upper triangular
183                assert_eq!(tc.reachable(i, j).expect("reach"), expected, "({i},{j})");
184            }
185        }
186    }
187
188    #[test]
189    fn chain_upper_triangular_reflexive() {
190        let g = chain(4);
191        let tc = transitive_closure(&g, true).expect("tc");
192        for i in 0..4 {
193            for j in 0..4 {
194                let expected = i <= j; // upper triangular incl. diagonal
195                assert_eq!(tc.reachable(i, j).expect("reach"), expected, "({i},{j})");
196            }
197        }
198    }
199
200    #[test]
201    fn small_dag_full_matrix() {
202        // 0 -> 1, 0 -> 2, 1 -> 3, 2 -> 3.
203        let mut g = AdjacencyList::new(4);
204        g.add_edge(0, 1).expect("edge");
205        g.add_edge(0, 2).expect("edge");
206        g.add_edge(1, 3).expect("edge");
207        g.add_edge(2, 3).expect("edge");
208        let tc = transitive_closure(&g, false).expect("tc");
209        // 0 reaches 1,2,3; 1 reaches 3; 2 reaches 3; 3 reaches nothing.
210        #[rustfmt::skip]
211        let expected: Vec<bool> = vec![
212            false, true,  true,  true,
213            false, false, false, true,
214            false, false, false, true,
215            false, false, false, false,
216        ];
217        assert_eq!(tc.matrix(), expected.as_slice());
218    }
219
220    #[test]
221    fn fw_and_bfs_agree_on_several_graphs() {
222        // Several deterministic digraphs; both methods must produce identical
223        // matrices for both reflexive settings.
224        let mut graphs: Vec<AdjacencyList> = Vec::new();
225
226        // g1: chain.
227        graphs.push(chain(6));
228
229        // g2: a DAG with cross edges.
230        let mut g2 = AdjacencyList::new(5);
231        for (u, v) in [(0, 1), (0, 2), (1, 3), (2, 3), (3, 4), (0, 4)] {
232            g2.add_edge(u, v).expect("edge");
233        }
234        graphs.push(g2);
235
236        // g3: contains a cycle 0->1->2->0 plus a tail to 3.
237        let mut g3 = AdjacencyList::new(4);
238        for (u, v) in [(0, 1), (1, 2), (2, 0), (2, 3)] {
239            g3.add_edge(u, v).expect("edge");
240        }
241        graphs.push(g3);
242
243        // g4: two disjoint components 0->1 and 2->3->4.
244        let mut g4 = AdjacencyList::new(5);
245        for (u, v) in [(0, 1), (2, 3), (3, 4)] {
246            g4.add_edge(u, v).expect("edge");
247        }
248        graphs.push(g4);
249
250        // g5: deterministic pseudo-random-ish digraph (LCG-style index walk).
251        let mut g5 = AdjacencyList::new(8);
252        let mut state = 1u64;
253        for _ in 0..20 {
254            state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
255            let u = (state >> 33) as usize % 8;
256            state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
257            let v = (state >> 33) as usize % 8;
258            g5.add_edge(u, v).expect("edge");
259        }
260        graphs.push(g5);
261
262        for g in &graphs {
263            for &reflexive in &[false, true] {
264                let a = transitive_closure(g, reflexive).expect("fw");
265                let b = transitive_closure_bfs(g, reflexive).expect("bfs");
266                assert_eq!(a.matrix(), b.matrix(), "mismatch (reflexive={reflexive})");
267            }
268        }
269    }
270
271    #[test]
272    fn cycle_everyone_reaches_everyone() {
273        // 0 -> 1 -> 2 -> 0.
274        let mut g = AdjacencyList::new(3);
275        g.add_edge(0, 1).expect("edge");
276        g.add_edge(1, 2).expect("edge");
277        g.add_edge(2, 0).expect("edge");
278        let tc = transitive_closure(&g, false).expect("tc");
279        for i in 0..3 {
280            for j in 0..3 {
281                assert!(tc.reachable(i, j).expect("reach"), "({i},{j})");
282            }
283        }
284        // Diagonal true even without reflexive flag, due to the cycle.
285        for v in 0..3 {
286            assert!(tc.reachable(v, v).expect("reach"));
287        }
288        // BFS variant agrees on the cyclic diagonal.
289        let tb = transitive_closure_bfs(&g, false).expect("bfs");
290        for v in 0..3 {
291            assert!(tb.reachable(v, v).expect("reach"));
292        }
293    }
294
295    #[test]
296    fn reflexive_flag_respected_with_isolated_vertex() {
297        // 0 -> 1, vertex 2 isolated, no self-loops.
298        let mut g = AdjacencyList::new(3);
299        g.add_edge(0, 1).expect("edge");
300
301        let irreflexive = transitive_closure(&g, false).expect("tc");
302        // Without the flag, no diagonal entries (no cycles / self-loops).
303        for v in 0..3 {
304            assert!(!irreflexive.reachable(v, v).expect("reach"), "diag {v}");
305        }
306
307        let reflexive = transitive_closure(&g, true).expect("tc");
308        for v in 0..3 {
309            assert!(reflexive.reachable(v, v).expect("reach"), "diag {v}");
310        }
311        // The isolated vertex 2 reaches only itself (and only when reflexive).
312        assert!(!reflexive.reachable(2, 0).expect("reach"));
313        assert!(!reflexive.reachable(2, 1).expect("reach"));
314    }
315
316    #[test]
317    fn reachable_bounds_check() {
318        let g = chain(3);
319        let tc = transitive_closure(&g, false).expect("tc");
320        assert!(matches!(
321            tc.reachable(3, 0),
322            Err(GraphalgError::IndexOutOfBounds { index: 3, len: 3 })
323        ));
324        assert!(matches!(
325            tc.reachable(0, 5),
326            Err(GraphalgError::IndexOutOfBounds { index: 5, len: 3 })
327        ));
328    }
329
330    #[test]
331    fn empty_graph_closure() {
332        let g = AdjacencyList::new(0);
333        let tc = transitive_closure(&g, true).expect("tc");
334        assert_eq!(tc.n, 0);
335        assert!(tc.matrix().is_empty());
336        let tb = transitive_closure_bfs(&g, true).expect("bfs");
337        assert!(tb.matrix().is_empty());
338    }
339
340    #[test]
341    fn is_reflexive_reported() {
342        let g = chain(2);
343        assert!(transitive_closure(&g, true).expect("tc").is_reflexive());
344        assert!(!transitive_closure(&g, false).expect("tc").is_reflexive());
345    }
346}