Skip to main content

rust_igraph/algorithms/properties/
is_fork_free.rs

1//! Fork-free graph predicate (ALGO-PR-108).
2//!
3//! A graph is fork-free if it contains no induced fork (also called
4//! cross or star-1,1,2). The fork has 5 vertices and 4 edges: a center
5//! vertex with 3 neighbors, one of which has an additional pendant.
6//! Equivalently, the fork is `K_{1,3}` (claw) with one edge subdivided.
7//!
8//! Vertices: {c, a, b, d, e} where c-a, c-b, c-d, d-e are edges
9//! (4 edges total). No other edges among these 5 vertices.
10//!
11//! For directed graphs, the function returns `false`.
12
13use crate::core::{Graph, IgraphResult};
14
15/// Check whether a graph is fork-free (no induced fork / cross).
16///
17/// The fork is a claw (`K_{1,3}`) with one edge subdivided: center c
18/// adjacent to a, b, d, and d also adjacent to e. No other edges
19/// among {c, a, b, d, e}.
20///
21/// Returns `false` for directed graphs.
22///
23/// # Examples
24///
25/// ```
26/// use rust_igraph::{Graph, is_fork_free};
27///
28/// // Path `P_4` is fork-free
29/// let mut g = Graph::with_vertices(4);
30/// g.add_edge(0, 1).unwrap();
31/// g.add_edge(1, 2).unwrap();
32/// g.add_edge(2, 3).unwrap();
33/// assert!(is_fork_free(&g).unwrap());
34///
35/// // Fork: center 0, arms 1, 2, 3, pendant 3-4
36/// let mut g = Graph::with_vertices(5);
37/// g.add_edge(0, 1).unwrap();
38/// g.add_edge(0, 2).unwrap();
39/// g.add_edge(0, 3).unwrap();
40/// g.add_edge(3, 4).unwrap();
41/// assert!(!is_fork_free(&g).unwrap());
42/// ```
43pub fn is_fork_free(graph: &Graph) -> IgraphResult<bool> {
44    if graph.is_directed() {
45        return Ok(false);
46    }
47
48    let n = graph.vcount();
49    if n < 5 {
50        return Ok(true);
51    }
52
53    let n_usize = n as usize;
54    let mut adj = vec![vec![false; n_usize]; n_usize];
55    let mut nbrs_list: Vec<Vec<u32>> = Vec::with_capacity(n_usize);
56
57    for v in 0..n {
58        let nbrs = graph.neighbors(v)?;
59        for &w in &nbrs {
60            adj[v as usize][w as usize] = true;
61        }
62        nbrs_list.push(nbrs);
63    }
64
65    // Fork: center c with neighbors {a, b, d} where a, b, d pairwise
66    // non-adjacent (induced claw), and d has a neighbor e outside
67    // {c, a, b, d} not adjacent to c, a, or b.
68    //
69    // Strategy: for each vertex c with degree ≥ 3, find triples of
70    // pairwise non-adjacent neighbors (induced claw). For each such
71    // triple, check if any of the three arms has a pendant forming
72    // the fork.
73    for c in 0..n {
74        let ci = c as usize;
75        let cn = &nbrs_list[ci];
76        if cn.len() < 3 {
77            continue;
78        }
79
80        for (i, &a) in cn.iter().enumerate() {
81            let ai = a as usize;
82            for (j, &b) in cn.iter().enumerate().skip(i + 1) {
83                let bi = b as usize;
84                if adj[ai][bi] {
85                    continue;
86                }
87                for &d in &cn[(j + 1)..] {
88                    let di = d as usize;
89                    if adj[ai][di] || adj[bi][di] {
90                        continue;
91                    }
92                    // Induced claw: c-{a,b,d}. Check each arm for pendant.
93                    if has_fork_pendant(&adj, &nbrs_list, ai, ci, bi, di) {
94                        return Ok(false);
95                    }
96                    if has_fork_pendant(&adj, &nbrs_list, bi, ci, ai, di) {
97                        return Ok(false);
98                    }
99                    if has_fork_pendant(&adj, &nbrs_list, di, ci, ai, bi) {
100                        return Ok(false);
101                    }
102                }
103            }
104        }
105    }
106
107    Ok(true)
108}
109
110/// Check if arm vertex has a pendant neighbor outside the claw.
111fn has_fork_pendant(
112    adj: &[Vec<bool>],
113    nbrs_list: &[Vec<u32>],
114    arm: usize,
115    center: usize,
116    other1: usize,
117    other2: usize,
118) -> bool {
119    for &e_u32 in &nbrs_list[arm] {
120        let e = e_u32 as usize;
121        if e == center || e == other1 || e == other2 {
122            continue;
123        }
124        if !adj[center][e] && !adj[other1][e] && !adj[other2][e] {
125            return true;
126        }
127    }
128    false
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn empty_graph() {
137        let g = Graph::with_vertices(0);
138        assert!(is_fork_free(&g).unwrap());
139    }
140
141    #[test]
142    fn small_graphs() {
143        let g = Graph::with_vertices(4);
144        assert!(is_fork_free(&g).unwrap());
145    }
146
147    #[test]
148    fn triangle() {
149        let mut g = Graph::with_vertices(3);
150        g.add_edge(0, 1).unwrap();
151        g.add_edge(1, 2).unwrap();
152        g.add_edge(2, 0).unwrap();
153        assert!(is_fork_free(&g).unwrap());
154    }
155
156    #[test]
157    fn fork() {
158        // Center 0, arms 1,2,3, pendant 3-4
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(3, 4).unwrap();
164        assert!(!is_fork_free(&g).unwrap());
165    }
166
167    #[test]
168    fn claw_is_fork_free() {
169        // Claw `K_{1,3}`: only 4 vertices, no room for pendant
170        let mut g = Graph::with_vertices(4);
171        g.add_edge(0, 1).unwrap();
172        g.add_edge(0, 2).unwrap();
173        g.add_edge(0, 3).unwrap();
174        assert!(is_fork_free(&g).unwrap());
175    }
176
177    #[test]
178    fn claw_with_pendant_is_fork() {
179        // Claw + pendant = fork
180        let mut g = Graph::with_vertices(5);
181        g.add_edge(0, 1).unwrap();
182        g.add_edge(0, 2).unwrap();
183        g.add_edge(0, 3).unwrap();
184        g.add_edge(1, 4).unwrap();
185        assert!(!is_fork_free(&g).unwrap());
186    }
187
188    #[test]
189    fn k5_fork_free() {
190        // `K_5`: no induced claw → no fork
191        let mut g = Graph::with_vertices(5);
192        for i in 0..5u32 {
193            for j in (i + 1)..5 {
194                g.add_edge(i, j).unwrap();
195            }
196        }
197        assert!(is_fork_free(&g).unwrap());
198    }
199
200    #[test]
201    fn path_p5_not_fork_free() {
202        // `P_5`: 0-1-2-3-4. Vertex 2 has neighbors {1,3}. Only degree 2,
203        // not enough for claw. No vertex has degree ≥ 3. So no claw → fork-free.
204        let mut g = Graph::with_vertices(5);
205        g.add_edge(0, 1).unwrap();
206        g.add_edge(1, 2).unwrap();
207        g.add_edge(2, 3).unwrap();
208        g.add_edge(3, 4).unwrap();
209        assert!(is_fork_free(&g).unwrap());
210    }
211
212    #[test]
213    fn star_with_subdivided_edge() {
214        // Star `S_4` (center 0, leaves 1,2,3,4) with edge 0-1 subdivided:
215        // 0-5-1, 0-2, 0-3, 0-4. Center 0 has neighbors {5,2,3,4} → claw
216        // {0,5,2,3}. Does 5 have a pendant? 5-1 edge, 1 not adj to 0... wait,
217        // we removed 0-1 and added 0-5, 5-1. So 0's neighbors are {5,2,3,4}.
218        // 5's neighbors are {0,1}. Claw on {0; 5,2,3}: 5 has pendant 1.
219        // 1 not adj to 0? Yes (we removed that edge). Not adj to 2,3? Yes.
220        // So this IS a fork.
221        let mut g = Graph::with_vertices(6);
222        g.add_edge(0, 5).unwrap();
223        g.add_edge(5, 1).unwrap();
224        g.add_edge(0, 2).unwrap();
225        g.add_edge(0, 3).unwrap();
226        g.add_edge(0, 4).unwrap();
227        assert!(!is_fork_free(&g).unwrap());
228    }
229
230    #[test]
231    fn directed_returns_false() {
232        let mut g = Graph::new(3, true).unwrap();
233        g.add_edge(0, 1).unwrap();
234        g.add_edge(1, 2).unwrap();
235        g.add_edge(2, 0).unwrap();
236        assert!(!is_fork_free(&g).unwrap());
237    }
238
239    #[test]
240    fn pendant_adjacent_to_another_arm() {
241        // Claw center 0 arms {1,2,3}, pendant from 3: 3-4.
242        // But 4 adjacent to 1 → not an induced fork (4-1 edge present)
243        let mut g = Graph::with_vertices(5);
244        g.add_edge(0, 1).unwrap();
245        g.add_edge(0, 2).unwrap();
246        g.add_edge(0, 3).unwrap();
247        g.add_edge(3, 4).unwrap();
248        g.add_edge(1, 4).unwrap();
249        assert!(is_fork_free(&g).unwrap());
250    }
251
252    #[test]
253    fn c5_fork_free() {
254        // `C_5`: max degree 2, no claw → fork-free
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, 3).unwrap();
259        g.add_edge(3, 4).unwrap();
260        g.add_edge(4, 0).unwrap();
261        assert!(is_fork_free(&g).unwrap());
262    }
263
264    #[test]
265    fn star_s5_is_fork_free() {
266        // `S_5`: center 0, leaves 1-5. Degree of center is 5.
267        // Claw triples: e.g. {1,2,3}. Pendant of 1? 1 has degree 1 (only
268        // neighbor is 0), so no pendant → fork-free.
269        let mut g = Graph::with_vertices(6);
270        g.add_edge(0, 1).unwrap();
271        g.add_edge(0, 2).unwrap();
272        g.add_edge(0, 3).unwrap();
273        g.add_edge(0, 4).unwrap();
274        g.add_edge(0, 5).unwrap();
275        assert!(is_fork_free(&g).unwrap());
276    }
277}