Skip to main content

rust_igraph/algorithms/properties/
is_bull_free.rs

1//! Bull-free graph predicate (ALGO-PR-100).
2//!
3//! A graph is bull-free if it contains no induced subgraph isomorphic
4//! to the bull graph. The bull has 5 vertices: a triangle {a, b, c}
5//! plus two pendant edges b-d and c-e (d and e each have degree 1).
6//! It has 5 edges total.
7//!
8//! For directed graphs, the function returns `false`.
9
10use crate::core::{Graph, IgraphResult};
11
12/// Check whether a graph is bull-free.
13///
14/// The bull graph is a triangle with two pendant edges from distinct
15/// triangle vertices. A bull-free graph has no induced bull.
16///
17/// Returns `false` for directed graphs.
18///
19/// # Examples
20///
21/// ```
22/// use rust_igraph::{Graph, is_bull_free};
23///
24/// // Triangle is bull-free
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_bull_free(&g).unwrap());
30///
31/// // Bull: triangle 0-1-2, pendants 1-3 and 2-4
32/// let mut g = Graph::with_vertices(5);
33/// g.add_edge(0, 1).unwrap();
34/// g.add_edge(0, 2).unwrap();
35/// g.add_edge(1, 2).unwrap();
36/// g.add_edge(1, 3).unwrap();
37/// g.add_edge(2, 4).unwrap();
38/// assert!(!is_bull_free(&g).unwrap());
39/// ```
40pub fn is_bull_free(graph: &Graph) -> IgraphResult<bool> {
41    if graph.is_directed() {
42        return Ok(false);
43    }
44
45    let n = graph.vcount();
46    if n < 5 {
47        return Ok(true);
48    }
49
50    let n_usize = n as usize;
51    let mut adj = vec![vec![false; n_usize]; n_usize];
52    for v in 0..n {
53        let nbrs = graph.neighbors(v)?;
54        for &w in &nbrs {
55            adj[v as usize][w as usize] = true;
56        }
57    }
58
59    // A bull is: triangle {a, b, c} with vertices d adjacent to b
60    // (but not a, c, e) and e adjacent to c (but not a, b, d).
61    // Strategy: for each triangle {a, b, c}, check if two distinct
62    // triangle vertices each have a neighbor outside the triangle
63    // that is not adjacent to any other triangle vertex or each other.
64    for a in 0..n {
65        let ai = a as usize;
66        for b in (a + 1)..n {
67            let bi = b as usize;
68            if !adj[ai][bi] {
69                continue;
70            }
71            for c in (b + 1)..n {
72                let ci = c as usize;
73                if !adj[ai][ci] || !adj[bi][ci] {
74                    continue;
75                }
76
77                // Triangle {a, b, c}. Check all 3 pairs of triangle
78                // vertices for pendant neighbors forming a bull.
79                // Try (b pendant d, c pendant e):
80                if has_bull_pendants(&adj, n, ai, bi, ci) {
81                    return Ok(false);
82                }
83                // Try (a pendant d, c pendant e):
84                if has_bull_pendants(&adj, n, bi, ai, ci) {
85                    return Ok(false);
86                }
87                // Try (a pendant d, b pendant e):
88                if has_bull_pendants(&adj, n, ci, ai, bi) {
89                    return Ok(false);
90                }
91            }
92        }
93    }
94
95    Ok(true)
96}
97
98/// Check if triangle vertices (apex, v1, v2) have pendant neighbors
99/// d (of v1) and e (of v2) forming a bull.
100/// d must be adjacent to v1 only (not apex, v2, or e).
101/// e must be adjacent to v2 only (not apex, v1, or d).
102fn has_bull_pendants(adj: &[Vec<bool>], _n: u32, apex: usize, v1: usize, v2: usize) -> bool {
103    let row_v1 = &adj[v1];
104    let row_apex = &adj[apex];
105    let row_v2 = &adj[v2];
106
107    let d_candidates: Vec<usize> = row_v1
108        .iter()
109        .enumerate()
110        .filter(|&(d, &is_adj)| {
111            is_adj && d != apex && d != v1 && d != v2 && !row_apex[d] && !row_v2[d]
112        })
113        .map(|(d, _)| d)
114        .collect();
115
116    if d_candidates.is_empty() {
117        return false;
118    }
119
120    for (e, &v2_to_e) in row_v2.iter().enumerate() {
121        if !v2_to_e || e == apex || e == v1 || e == v2 {
122            continue;
123        }
124        if !row_apex[e] && !row_v1[e] {
125            for &d in &d_candidates {
126                if d != e && !adj[d][e] {
127                    return true;
128                }
129            }
130        }
131    }
132
133    false
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn empty_graph() {
142        let g = Graph::with_vertices(0);
143        assert!(is_bull_free(&g).unwrap());
144    }
145
146    #[test]
147    fn small_graphs() {
148        let g = Graph::with_vertices(4);
149        assert!(is_bull_free(&g).unwrap());
150    }
151
152    #[test]
153    fn triangle() {
154        let mut g = Graph::with_vertices(3);
155        g.add_edge(0, 1).unwrap();
156        g.add_edge(1, 2).unwrap();
157        g.add_edge(2, 0).unwrap();
158        assert!(is_bull_free(&g).unwrap());
159    }
160
161    #[test]
162    fn bull() {
163        // Triangle 0-1-2, pendants 1-3, 2-4
164        let mut g = Graph::with_vertices(5);
165        g.add_edge(0, 1).unwrap();
166        g.add_edge(0, 2).unwrap();
167        g.add_edge(1, 2).unwrap();
168        g.add_edge(1, 3).unwrap();
169        g.add_edge(2, 4).unwrap();
170        assert!(!is_bull_free(&g).unwrap());
171    }
172
173    #[test]
174    fn k5_bull_free() {
175        // Complete graphs are bull-free (every 5-vertex subgraph is K_5)
176        let mut g = Graph::with_vertices(5);
177        for i in 0..5u32 {
178            for j in (i + 1)..5 {
179                g.add_edge(i, j).unwrap();
180            }
181        }
182        assert!(is_bull_free(&g).unwrap());
183    }
184
185    #[test]
186    fn c5_bull_free() {
187        // C_5 has no triangles → no bull
188        let mut g = Graph::with_vertices(5);
189        g.add_edge(0, 1).unwrap();
190        g.add_edge(1, 2).unwrap();
191        g.add_edge(2, 3).unwrap();
192        g.add_edge(3, 4).unwrap();
193        g.add_edge(4, 0).unwrap();
194        assert!(is_bull_free(&g).unwrap());
195    }
196
197    #[test]
198    fn path_bull_free() {
199        let mut g = Graph::with_vertices(5);
200        g.add_edge(0, 1).unwrap();
201        g.add_edge(1, 2).unwrap();
202        g.add_edge(2, 3).unwrap();
203        g.add_edge(3, 4).unwrap();
204        assert!(is_bull_free(&g).unwrap());
205    }
206
207    #[test]
208    fn paw_bull_free() {
209        // Paw: triangle 0-1-2 + pendant 2-3
210        // Only one pendant → not a bull (need two)
211        let mut g = Graph::with_vertices(4);
212        g.add_edge(0, 1).unwrap();
213        g.add_edge(1, 2).unwrap();
214        g.add_edge(2, 0).unwrap();
215        g.add_edge(2, 3).unwrap();
216        assert!(is_bull_free(&g).unwrap());
217    }
218
219    #[test]
220    fn diamond_bull_free() {
221        // Diamond: K_4 minus one edge
222        let mut g = Graph::with_vertices(4);
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(1, 2).unwrap();
227        g.add_edge(1, 3).unwrap();
228        assert!(is_bull_free(&g).unwrap());
229    }
230
231    #[test]
232    fn directed_returns_false() {
233        let mut g = Graph::new(3, true).unwrap();
234        g.add_edge(0, 1).unwrap();
235        g.add_edge(1, 2).unwrap();
236        g.add_edge(2, 0).unwrap();
237        assert!(!is_bull_free(&g).unwrap());
238    }
239
240    #[test]
241    fn star_bull_free() {
242        let mut g = Graph::with_vertices(6);
243        g.add_edge(0, 1).unwrap();
244        g.add_edge(0, 2).unwrap();
245        g.add_edge(0, 3).unwrap();
246        g.add_edge(0, 4).unwrap();
247        g.add_edge(0, 5).unwrap();
248        assert!(is_bull_free(&g).unwrap());
249    }
250
251    #[test]
252    fn triangle_with_two_pendants_on_same_vertex() {
253        // Triangle 0-1-2, two pendants from vertex 1: 1-3, 1-4
254        // Not a bull (pendants must be from different triangle vertices)
255        let mut g = Graph::with_vertices(5);
256        g.add_edge(0, 1).unwrap();
257        g.add_edge(1, 2).unwrap();
258        g.add_edge(2, 0).unwrap();
259        g.add_edge(1, 3).unwrap();
260        g.add_edge(1, 4).unwrap();
261        // Triangle {0,1,2}: pendants of 1 are 3,4. But we need pendants
262        // from two DIFFERENT triangle vertices. Vertex 0 has no pendant,
263        // vertex 2 has no pendant. So no bull.
264        assert!(is_bull_free(&g).unwrap());
265    }
266
267    #[test]
268    fn connected_pendants_not_bull() {
269        // Triangle 0-1-2, pendants 1-3, 2-4, but 3-4 adjacent
270        // The induced subgraph {0,1,2,3,4} has 3-4 edge → not a bull
271        let mut g = Graph::with_vertices(5);
272        g.add_edge(0, 1).unwrap();
273        g.add_edge(0, 2).unwrap();
274        g.add_edge(1, 2).unwrap();
275        g.add_edge(1, 3).unwrap();
276        g.add_edge(2, 4).unwrap();
277        g.add_edge(3, 4).unwrap();
278        assert!(is_bull_free(&g).unwrap());
279    }
280}