Skip to main content

rust_igraph/algorithms/properties/
is_gem_free.rs

1//! Gem-free graph predicate (ALGO-PR-098).
2//!
3//! A graph is gem-free if it contains no induced subgraph isomorphic
4//! to the gem graph (also called the fan `F_{1,3}`): a `P_4` plus a
5//! universal vertex adjacent to all four path vertices. The gem has
6//! 5 vertices and 7 edges.
7//!
8//! For directed graphs, the function returns `false`.
9
10use crate::core::{Graph, IgraphResult};
11
12/// Check whether a graph is gem-free.
13///
14/// The gem (fan `F_{1,3}`) is `P_4` plus a vertex adjacent to all
15/// four path vertices. A gem-free graph has no induced gem.
16///
17/// Returns `false` for directed graphs.
18///
19/// # Examples
20///
21/// ```
22/// use rust_igraph::{Graph, is_gem_free};
23///
24/// // Triangle is gem-free (too few vertices for a gem)
25/// let mut g = Graph::with_vertices(3);
26/// g.add_edge(0, 1).unwrap();
27/// g.add_edge(1, 2).unwrap();
28/// g.add_edge(2, 0).unwrap();
29/// assert!(is_gem_free(&g).unwrap());
30/// ```
31pub fn is_gem_free(graph: &Graph) -> IgraphResult<bool> {
32    if graph.is_directed() {
33        return Ok(false);
34    }
35
36    let n = graph.vcount();
37    if n < 5 {
38        return Ok(true);
39    }
40
41    let n_usize = n as usize;
42    let mut adj = vec![vec![false; n_usize]; n_usize];
43    let mut nbrs_list: Vec<Vec<u32>> = Vec::with_capacity(n_usize);
44
45    for v in 0..n {
46        let nbrs = graph.neighbors(v)?;
47        for &w in &nbrs {
48            adj[v as usize][w as usize] = true;
49        }
50        nbrs_list.push(nbrs);
51    }
52
53    // A gem is: vertices {u, a, b, c, d} where a-b-c-d is an induced P_4
54    // and u is adjacent to all of {a, b, c, d}.
55    // Strategy: for each vertex u with degree ≥ 4, check if N(u)
56    // contains an induced P_4.
57    for u in 0..n {
58        let u_idx = u as usize;
59        if nbrs_list[u_idx].len() < 4 {
60            continue;
61        }
62
63        let nbrs_u = &nbrs_list[u_idx];
64
65        // Check all ordered 4-tuples (a, b, c, d) from N(u) for induced P_4
66        // a-b adjacent, b-c adjacent, c-d adjacent,
67        // a-c NOT adjacent, b-d NOT adjacent, a-d NOT adjacent
68        for (i, &a) in nbrs_u.iter().enumerate() {
69            let ai = a as usize;
70            for &b in &nbrs_u[i + 1..] {
71                let bi = b as usize;
72                if !adj[ai][bi] {
73                    continue;
74                }
75                // a-b adjacent; look for c adjacent to b but not a
76                for &c in nbrs_u {
77                    if c == a || c == b {
78                        continue;
79                    }
80                    let ci = c as usize;
81                    if !adj[bi][ci] || adj[ai][ci] {
82                        continue;
83                    }
84                    // a-b-c is an induced P_3; look for d adjacent to c
85                    // but not a and not b
86                    for &d in nbrs_u {
87                        if d == a || d == b || d == c {
88                            continue;
89                        }
90                        let di = d as usize;
91                        if adj[ci][di] && !adj[bi][di] && !adj[ai][di] {
92                            return Ok(false);
93                        }
94                    }
95                }
96            }
97        }
98    }
99
100    Ok(true)
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn empty_graph() {
109        let g = Graph::with_vertices(0);
110        assert!(is_gem_free(&g).unwrap());
111    }
112
113    #[test]
114    fn small_graphs() {
115        let g = Graph::with_vertices(4);
116        assert!(is_gem_free(&g).unwrap());
117    }
118
119    #[test]
120    fn triangle() {
121        let mut g = Graph::with_vertices(3);
122        g.add_edge(0, 1).unwrap();
123        g.add_edge(1, 2).unwrap();
124        g.add_edge(2, 0).unwrap();
125        assert!(is_gem_free(&g).unwrap());
126    }
127
128    #[test]
129    fn gem_graph() {
130        // Gem: P_4 = 1-2-3-4, universal vertex 0
131        let mut g = Graph::with_vertices(5);
132        g.add_edge(0, 1).unwrap();
133        g.add_edge(0, 2).unwrap();
134        g.add_edge(0, 3).unwrap();
135        g.add_edge(0, 4).unwrap();
136        g.add_edge(1, 2).unwrap();
137        g.add_edge(2, 3).unwrap();
138        g.add_edge(3, 4).unwrap();
139        assert!(!is_gem_free(&g).unwrap());
140    }
141
142    #[test]
143    fn k5() {
144        // K_5 is gem-free: every 5-vertex induced subgraph is K_5,
145        // which has too many edges for a gem
146        let mut g = Graph::with_vertices(5);
147        for i in 0..5u32 {
148            for j in (i + 1)..5 {
149                g.add_edge(i, j).unwrap();
150            }
151        }
152        assert!(is_gem_free(&g).unwrap());
153    }
154
155    #[test]
156    fn star_k15() {
157        // Star K_{1,4}: vertex 0 connected to 1,2,3,4
158        // N(0) = {1,2,3,4}, all mutually non-adjacent → no P_4 in N(0)
159        let mut g = Graph::with_vertices(5);
160        g.add_edge(0, 1).unwrap();
161        g.add_edge(0, 2).unwrap();
162        g.add_edge(0, 3).unwrap();
163        g.add_edge(0, 4).unwrap();
164        assert!(is_gem_free(&g).unwrap());
165    }
166
167    #[test]
168    fn c5_gem_free() {
169        let mut g = Graph::with_vertices(5);
170        g.add_edge(0, 1).unwrap();
171        g.add_edge(1, 2).unwrap();
172        g.add_edge(2, 3).unwrap();
173        g.add_edge(3, 4).unwrap();
174        g.add_edge(4, 0).unwrap();
175        assert!(is_gem_free(&g).unwrap());
176    }
177
178    #[test]
179    fn path_p5_gem_free() {
180        let mut g = Graph::with_vertices(5);
181        g.add_edge(0, 1).unwrap();
182        g.add_edge(1, 2).unwrap();
183        g.add_edge(2, 3).unwrap();
184        g.add_edge(3, 4).unwrap();
185        assert!(is_gem_free(&g).unwrap());
186    }
187
188    #[test]
189    fn wheel_w5_not_gem_free() {
190        // W_5: hub 0, rim 1-2-3-4-5-1
191        let mut g = Graph::with_vertices(6);
192        g.add_edge(0, 1).unwrap();
193        g.add_edge(0, 2).unwrap();
194        g.add_edge(0, 3).unwrap();
195        g.add_edge(0, 4).unwrap();
196        g.add_edge(0, 5).unwrap();
197        g.add_edge(1, 2).unwrap();
198        g.add_edge(2, 3).unwrap();
199        g.add_edge(3, 4).unwrap();
200        g.add_edge(4, 5).unwrap();
201        g.add_edge(5, 1).unwrap();
202        // Hub 0 has N(0) = {1,2,3,4,5}
203        // In N(0): 1-2-3-4 is P_4 (1-2 adj, 2-3 adj, 3-4 adj,
204        // 1-3 not adj, 2-4 not adj, 1-4 not adj via rim) → gem
205        assert!(!is_gem_free(&g).unwrap());
206    }
207
208    #[test]
209    fn directed_returns_false() {
210        let mut g = Graph::new(3, true).unwrap();
211        g.add_edge(0, 1).unwrap();
212        g.add_edge(1, 2).unwrap();
213        g.add_edge(2, 0).unwrap();
214        assert!(!is_gem_free(&g).unwrap());
215    }
216
217    #[test]
218    fn fan_3_plus_universal() {
219        // Universal vertex 0, path 1-2-3, plus extra vertex 4 connected to 0 only
220        // N(0) = {1,2,3,4}. In N(0): 1-2-3 but 4 isolated from 1,2,3
221        // No P_4 in N(0) since 4 is isolated
222        let mut g = Graph::with_vertices(5);
223        g.add_edge(0, 1).unwrap();
224        g.add_edge(0, 2).unwrap();
225        g.add_edge(0, 3).unwrap();
226        g.add_edge(0, 4).unwrap();
227        g.add_edge(1, 2).unwrap();
228        g.add_edge(2, 3).unwrap();
229        assert!(is_gem_free(&g).unwrap());
230    }
231}