Skip to main content

rust_igraph/algorithms/properties/
is_dag.rs

1//! DAG predicate (ALGO-PR-020).
2//!
3//! Counterpart of `igraph_is_dag()` from
4//! `references/igraph/src/properties/dag.c:151`. Returns `true` iff
5//! `graph` is a directed acyclic graph. Undirected graphs are
6//! always `false` (a DAG must be directed by definition — matches
7//! upstream).
8//!
9//! Algorithm: Kahn's topological-sort. Repeatedly peel off
10//! zero-in-degree vertices until either the graph is empty (DAG)
11//! or every remaining vertex has at least one incoming edge
12//! (a cycle exists). Self-loops are detected as an early
13//! short-circuit because they contribute a vertex with
14//! `in-degree >= 1` that can never be peeled.
15//!
16//! Time complexity: `O(V + E)`.
17
18use std::collections::VecDeque;
19
20use crate::core::Graph;
21use crate::core::cache::CachedProperty;
22use crate::core::graph::VertexId;
23
24/// Returns `true` iff `graph` is a directed acyclic graph.
25///
26/// Undirected graphs return `false` unconditionally (matches
27/// upstream — DAGs are directed by definition). Empty graphs and
28/// graphs with isolated vertices but no edges return `true`.
29///
30/// Algorithm: Kahn's topological-sort peel. Start with vertices
31/// whose in-degree is 0 in a queue; pop one, "remove" it by
32/// decrementing the in-degree of its out-neighbours. If any
33/// neighbour reaches 0, queue it. A self-loop is short-circuited
34/// to `false` because the loop's tail can never have its in-degree
35/// reduced past itself.
36///
37/// Counterpart of `igraph_is_dag` from
38/// `references/igraph/src/properties/dag.c:151`.
39///
40/// # Examples
41///
42/// ```
43/// use rust_igraph::{Graph, is_dag};
44///
45/// // Linear chain 0 → 1 → 2 — a DAG.
46/// let mut g = Graph::new(3, true).unwrap();
47/// g.add_edge(0, 1).unwrap();
48/// g.add_edge(1, 2).unwrap();
49/// assert!(is_dag(&g));
50///
51/// // Cycle 0 → 1 → 2 → 0 — not a DAG.
52/// let mut g = Graph::new(3, true).unwrap();
53/// g.add_edge(0, 1).unwrap();
54/// g.add_edge(1, 2).unwrap();
55/// g.add_edge(2, 0).unwrap();
56/// assert!(!is_dag(&g));
57/// ```
58#[must_use]
59pub fn is_dag(graph: &Graph) -> bool {
60    if !graph.is_directed() {
61        return false;
62    }
63    if let Some(v) = graph.cache_get(CachedProperty::IsDag) {
64        return v;
65    }
66
67    let n = graph.vcount();
68    let n_us = n as usize;
69
70    // In-degree of every vertex. `incident_in(v)` returns the in-edges
71    // on a directed graph; its length is the in-degree.
72    let mut in_deg: Vec<u32> = Vec::with_capacity(n_us);
73    for v in 0..n {
74        let Ok(in_edges) = graph.incident_in(v) else {
75            return false;
76        };
77        let deg = u32::try_from(in_edges.len()).unwrap_or(u32::MAX);
78        in_deg.push(deg);
79    }
80
81    // Queue of vertices with current in-degree 0.
82    let mut sources: VecDeque<VertexId> = VecDeque::new();
83    for v in 0..n {
84        if in_deg[v as usize] == 0 {
85            sources.push_back(v);
86        }
87    }
88
89    let mut peeled: u32 = 0;
90    while let Some(v) = sources.pop_front() {
91        peeled = peeled.saturating_add(1);
92
93        // Walk out-edges; for each neighbour, drop its in-degree.
94        let Ok(out_eids) = graph.incident(v) else {
95            return false;
96        };
97        for eid in out_eids {
98            let Ok(nei) = graph.edge_other(eid, v) else {
99                return false;
100            };
101            if nei == v {
102                // Self-loop: vertex v depends on itself; it can
103                // never be peeled away. The graph has a cycle.
104                return false;
105            }
106            let nei_idx = nei as usize;
107            in_deg[nei_idx] = in_deg[nei_idx].saturating_sub(1);
108            if in_deg[nei_idx] == 0 {
109                sources.push_back(nei);
110            }
111        }
112    }
113
114    let result = peeled == n;
115    graph.cache_set(CachedProperty::IsDag, result);
116    result
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use crate::core::Graph;
123
124    #[test]
125    fn empty_directed_graph_is_dag() {
126        let g = Graph::new(0, true).unwrap();
127        assert!(is_dag(&g));
128    }
129
130    #[test]
131    fn isolated_vertices_only_is_dag() {
132        let g = Graph::new(5, true).unwrap();
133        assert!(is_dag(&g));
134    }
135
136    #[test]
137    fn linear_chain_is_dag() {
138        let mut g = Graph::new(4, true).unwrap();
139        g.add_edges(vec![(0u32, 1u32), (1, 2), (2, 3)]).unwrap();
140        assert!(is_dag(&g));
141    }
142
143    #[test]
144    fn binary_tree_is_dag() {
145        // 0 → 1, 0 → 2, 1 → 3, 1 → 4.
146        let mut g = Graph::new(5, true).unwrap();
147        g.add_edges(vec![(0u32, 1u32), (0, 2), (1, 3), (1, 4)])
148            .unwrap();
149        assert!(is_dag(&g));
150    }
151
152    #[test]
153    fn three_cycle_is_not_dag() {
154        let mut g = Graph::new(3, true).unwrap();
155        g.add_edges(vec![(0u32, 1u32), (1, 2), (2, 0)]).unwrap();
156        assert!(!is_dag(&g));
157    }
158
159    #[test]
160    fn self_loop_is_not_dag() {
161        let mut g = Graph::new(2, true).unwrap();
162        g.add_edge(0, 0).unwrap();
163        g.add_edge(0, 1).unwrap();
164        assert!(!is_dag(&g));
165    }
166
167    #[test]
168    fn undirected_graph_is_not_dag_even_if_acyclic() {
169        // Undirected path 0-1-2 — acyclic as a graph but the
170        // upstream contract is "DAGs are directed".
171        let mut g = Graph::with_vertices(3);
172        g.add_edges(vec![(0u32, 1u32), (1, 2)]).unwrap();
173        assert!(!is_dag(&g));
174    }
175
176    #[test]
177    fn dag_with_multiple_roots() {
178        // Two disjoint DAG branches feeding into a sink.
179        // 0 → 2, 1 → 2.
180        let mut g = Graph::new(3, true).unwrap();
181        g.add_edges(vec![(0u32, 2u32), (1, 2)]).unwrap();
182        assert!(is_dag(&g));
183    }
184
185    #[test]
186    fn dag_with_parallel_edges_still_dag() {
187        // Parallel edges 0→1 don't introduce a cycle.
188        let mut g = Graph::new(2, true).unwrap();
189        g.add_edge(0, 1).unwrap();
190        g.add_edge(0, 1).unwrap();
191        assert!(is_dag(&g));
192    }
193
194    #[test]
195    fn cycle_with_extra_tail_branches_not_dag() {
196        // Cycle 0→1→2→0 plus a tail 3→0. The tail can be peeled
197        // but the cycle remains, so the graph is not a DAG.
198        let mut g = Graph::new(4, true).unwrap();
199        g.add_edges(vec![(0u32, 1u32), (1, 2), (2, 0), (3, 0)])
200            .unwrap();
201        assert!(!is_dag(&g));
202    }
203
204    #[test]
205    fn back_edge_to_root_is_not_dag() {
206        // 0 → 1 → 2 → 0 (3-cycle) — different framing of the
207        // three_cycle case, double-check the back edge is detected.
208        let mut g = Graph::new(3, true).unwrap();
209        g.add_edges(vec![(0u32, 1u32), (1, 2), (2, 0)]).unwrap();
210        assert!(!is_dag(&g));
211    }
212
213    #[test]
214    fn two_disjoint_dags_is_dag() {
215        // Two unrelated 2-vertex DAGs in the same graph.
216        let mut g = Graph::new(4, true).unwrap();
217        g.add_edges(vec![(0u32, 1u32), (2, 3)]).unwrap();
218        assert!(is_dag(&g));
219    }
220}