Skip to main content

rust_igraph/algorithms/properties/
is_triangle_free.rs

1//! Triangle-free test (ALGO-PR-039).
2//!
3//! Counterpart of `igraph_is_triangle_free()` from
4//! `references/igraph/src/properties/triangles.c`.
5//!
6//! Checks whether the graph contains no triangles (3-cycles).
7
8use crate::core::{Graph, IgraphResult};
9
10/// Test whether a graph is triangle-free.
11///
12/// A graph is triangle-free if it contains no 3-clique (cycle of length 3).
13/// Edge directions, multi-edges, and self-loops are ignored for the purpose
14/// of this check (the underlying simple undirected graph is tested).
15///
16/// Uses sorted-neighbor-intersection with early exit for efficiency.
17///
18/// # Examples
19///
20/// ```
21/// use rust_igraph::{Graph, is_triangle_free};
22///
23/// // Path graph: no triangles
24/// let mut g = Graph::with_vertices(4);
25/// g.add_edge(0, 1).unwrap();
26/// g.add_edge(1, 2).unwrap();
27/// g.add_edge(2, 3).unwrap();
28/// assert!(is_triangle_free(&g).unwrap());
29///
30/// // Add edge 0-2 to create a triangle
31/// g.add_edge(0, 2).unwrap();
32/// assert!(!is_triangle_free(&g).unwrap());
33/// ```
34pub fn is_triangle_free(graph: &Graph) -> IgraphResult<bool> {
35    let n = graph.vcount() as usize;
36    let m = graph.ecount();
37
38    // Trivial cases: need at least 3 vertices and 3 edges for a triangle.
39    if n < 3 || m < 3 {
40        return Ok(true);
41    }
42
43    // Build simple undirected adjacency list (sorted, no loops, no multi-edges).
44    let adj = build_sorted_simple_adjlist(graph)?;
45
46    // For each edge (u, v) with u < v, check if their sorted neighbor
47    // lists intersect — if so, there's a triangle.
48    for u in 0..n {
49        let du = adj[u].len();
50        if du < 2 {
51            continue;
52        }
53        for &v in &adj[u] {
54            if v <= u {
55                continue;
56            }
57            let dv = adj[v].len();
58            if dv < 2 {
59                continue;
60            }
61
62            // Sorted intersection check.
63            if has_common_element(&adj[u], &adj[v]) {
64                return Ok(false);
65            }
66        }
67    }
68
69    Ok(true)
70}
71
72/// Check if two sorted slices have a common element.
73fn has_common_element(a: &[usize], b: &[usize]) -> bool {
74    let mut i = 0;
75    let mut j = 0;
76    while i < a.len() && j < b.len() {
77        match a[i].cmp(&b[j]) {
78            std::cmp::Ordering::Equal => return true,
79            std::cmp::Ordering::Less => i += 1,
80            std::cmp::Ordering::Greater => j += 1,
81        }
82    }
83    false
84}
85
86/// Build a simple undirected adjacency list (sorted, no loops, no multi-edges).
87fn build_sorted_simple_adjlist(graph: &Graph) -> IgraphResult<Vec<Vec<usize>>> {
88    let n = graph.vcount() as usize;
89    let m = graph.ecount();
90    let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
91
92    for eid_usize in 0..m {
93        #[allow(clippy::cast_possible_truncation)]
94        let eid = eid_usize as u32;
95        let (from, to) = graph.edge(eid)?;
96        let f = from as usize;
97        let t = to as usize;
98        if f != t {
99            adj[f].push(t);
100            adj[t].push(f);
101        }
102    }
103
104    for neighbors in &mut adj {
105        neighbors.sort_unstable();
106        neighbors.dedup();
107    }
108
109    Ok(adj)
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn empty_graph() {
118        let g = Graph::with_vertices(0);
119        assert!(is_triangle_free(&g).unwrap());
120    }
121
122    #[test]
123    fn single_vertex() {
124        let g = Graph::with_vertices(1);
125        assert!(is_triangle_free(&g).unwrap());
126    }
127
128    #[test]
129    fn single_edge() {
130        let mut g = Graph::with_vertices(2);
131        g.add_edge(0, 1).unwrap();
132        assert!(is_triangle_free(&g).unwrap());
133    }
134
135    #[test]
136    fn triangle() {
137        let mut g = Graph::with_vertices(3);
138        g.add_edge(0, 1).unwrap();
139        g.add_edge(1, 2).unwrap();
140        g.add_edge(2, 0).unwrap();
141        assert!(!is_triangle_free(&g).unwrap());
142    }
143
144    #[test]
145    fn path_is_triangle_free() {
146        let mut g = Graph::with_vertices(5);
147        g.add_edge(0, 1).unwrap();
148        g.add_edge(1, 2).unwrap();
149        g.add_edge(2, 3).unwrap();
150        g.add_edge(3, 4).unwrap();
151        assert!(is_triangle_free(&g).unwrap());
152    }
153
154    #[test]
155    fn cycle_4_is_triangle_free() {
156        let mut g = Graph::with_vertices(4);
157        g.add_edge(0, 1).unwrap();
158        g.add_edge(1, 2).unwrap();
159        g.add_edge(2, 3).unwrap();
160        g.add_edge(3, 0).unwrap();
161        assert!(is_triangle_free(&g).unwrap());
162    }
163
164    #[test]
165    fn cycle_5_is_triangle_free() {
166        let mut g = Graph::with_vertices(5);
167        g.add_edge(0, 1).unwrap();
168        g.add_edge(1, 2).unwrap();
169        g.add_edge(2, 3).unwrap();
170        g.add_edge(3, 4).unwrap();
171        g.add_edge(4, 0).unwrap();
172        assert!(is_triangle_free(&g).unwrap());
173    }
174
175    #[test]
176    fn complete_4_not_triangle_free() {
177        let mut g = Graph::with_vertices(4);
178        for i in 0..4u32 {
179            for j in (i + 1)..4 {
180                g.add_edge(i, j).unwrap();
181            }
182        }
183        assert!(!is_triangle_free(&g).unwrap());
184    }
185
186    #[test]
187    fn bipartite_is_triangle_free() {
188        // Complete bipartite K3,3 is triangle-free
189        let mut g = Graph::with_vertices(6);
190        for i in 0..3u32 {
191            for j in 3..6u32 {
192                g.add_edge(i, j).unwrap();
193            }
194        }
195        assert!(is_triangle_free(&g).unwrap());
196    }
197
198    #[test]
199    fn self_loops_ignored() {
200        let mut g = Graph::with_vertices(3);
201        g.add_edge(0, 0).unwrap();
202        g.add_edge(1, 1).unwrap();
203        g.add_edge(0, 1).unwrap();
204        assert!(is_triangle_free(&g).unwrap());
205    }
206
207    #[test]
208    fn multi_edges_ignored() {
209        let mut g = Graph::with_vertices(3);
210        g.add_edge(0, 1).unwrap();
211        g.add_edge(0, 1).unwrap();
212        g.add_edge(1, 2).unwrap();
213        // Multi-edges don't form a triangle
214        assert!(is_triangle_free(&g).unwrap());
215    }
216
217    #[test]
218    fn directed_ignores_direction() {
219        let mut g = Graph::new(3, true).unwrap();
220        g.add_edge(0, 1).unwrap();
221        g.add_edge(1, 2).unwrap();
222        g.add_edge(2, 0).unwrap();
223        // Treated as undirected -> triangle exists
224        assert!(!is_triangle_free(&g).unwrap());
225    }
226
227    #[test]
228    fn no_edges() {
229        let g = Graph::with_vertices(10);
230        assert!(is_triangle_free(&g).unwrap());
231    }
232
233    #[test]
234    fn star_is_triangle_free() {
235        let mut g = Graph::with_vertices(5);
236        g.add_edge(0, 1).unwrap();
237        g.add_edge(0, 2).unwrap();
238        g.add_edge(0, 3).unwrap();
239        g.add_edge(0, 4).unwrap();
240        assert!(is_triangle_free(&g).unwrap());
241    }
242
243    #[test]
244    fn petersen_is_triangle_free() {
245        // The Petersen graph is triangle-free
246        let mut g = Graph::with_vertices(10);
247        // Outer cycle
248        g.add_edge(0, 1).unwrap();
249        g.add_edge(1, 2).unwrap();
250        g.add_edge(2, 3).unwrap();
251        g.add_edge(3, 4).unwrap();
252        g.add_edge(4, 0).unwrap();
253        // Inner star
254        g.add_edge(5, 7).unwrap();
255        g.add_edge(7, 9).unwrap();
256        g.add_edge(9, 6).unwrap();
257        g.add_edge(6, 8).unwrap();
258        g.add_edge(8, 5).unwrap();
259        // Connections
260        g.add_edge(0, 5).unwrap();
261        g.add_edge(1, 6).unwrap();
262        g.add_edge(2, 7).unwrap();
263        g.add_edge(3, 8).unwrap();
264        g.add_edge(4, 9).unwrap();
265
266        assert!(is_triangle_free(&g).unwrap());
267    }
268}