rust_igraph/algorithms/properties/is_paw_free.rs
1//! Paw-free graph predicate (ALGO-PR-099).
2//!
3//! A graph is paw-free if it contains no induced subgraph isomorphic
4//! to the paw graph. The paw is a triangle with one pendant edge
5//! (4 vertices, 4 edges): vertices {a, b, c, d} with edges
6//! {ab, ac, bc, cd} (d is pendant, adjacent only to c).
7//!
8//! Paw-free graphs are exactly the graphs where every connected
9//! component is either triangle-free or complete.
10//!
11//! For directed graphs, the function returns `false`.
12
13use crate::core::{Graph, IgraphResult};
14
15/// Check whether a graph is paw-free.
16///
17/// The paw is a triangle plus a pendant edge. A paw-free graph has
18/// no induced paw. Equivalently, every connected component is
19/// either triangle-free or complete.
20///
21/// Returns `false` for directed graphs.
22///
23/// # Examples
24///
25/// ```
26/// use rust_igraph::{Graph, is_paw_free};
27///
28/// // Triangle is paw-free (no pendant vertex)
29/// let mut g = Graph::with_vertices(3);
30/// g.add_edge(0, 1).unwrap();
31/// g.add_edge(1, 2).unwrap();
32/// g.add_edge(2, 0).unwrap();
33/// assert!(is_paw_free(&g).unwrap());
34///
35/// // Triangle + pendant: paw!
36/// let mut g = Graph::with_vertices(4);
37/// g.add_edge(0, 1).unwrap();
38/// g.add_edge(1, 2).unwrap();
39/// g.add_edge(2, 0).unwrap();
40/// g.add_edge(2, 3).unwrap();
41/// assert!(!is_paw_free(&g).unwrap());
42/// ```
43pub fn is_paw_free(graph: &Graph) -> IgraphResult<bool> {
44 if graph.is_directed() {
45 return Ok(false);
46 }
47
48 let n = graph.vcount();
49 if n < 4 {
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 for v in 0..n {
56 let nbrs = graph.neighbors(v)?;
57 for &w in &nbrs {
58 adj[v as usize][w as usize] = true;
59 }
60 }
61
62 // Check for induced paw: find a triangle {a, b, c} where some
63 // vertex d is adjacent to exactly one of {a, b, c}.
64 // Equivalently: for each edge (a, b), find common neighbor c
65 // (forming a triangle), then check if any neighbor of a, b, or c
66 // is adjacent to exactly one member of the triangle.
67 for a in 0..n {
68 let ai = a as usize;
69 for b in (a + 1)..n {
70 let bi = b as usize;
71 if !adj[ai][bi] {
72 continue;
73 }
74
75 // Find common neighbors of a and b (forming triangles)
76 for c in (b + 1)..n {
77 let ci = c as usize;
78 if !adj[ai][ci] || !adj[bi][ci] {
79 continue;
80 }
81
82 // Triangle {a, b, c} found. Check if any vertex d
83 // is adjacent to exactly one of {a, b, c}.
84 for d in 0..n {
85 if d == a || d == b || d == c {
86 continue;
87 }
88 let di = d as usize;
89 let count =
90 u32::from(adj[ai][di]) + u32::from(adj[bi][di]) + u32::from(adj[ci][di]);
91 if count == 1 {
92 return Ok(false);
93 }
94 }
95 }
96 }
97 }
98
99 Ok(true)
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn empty_graph() {
108 let g = Graph::with_vertices(0);
109 assert!(is_paw_free(&g).unwrap());
110 }
111
112 #[test]
113 fn single_vertex() {
114 let g = Graph::with_vertices(1);
115 assert!(is_paw_free(&g).unwrap());
116 }
117
118 #[test]
119 fn triangle() {
120 let mut g = Graph::with_vertices(3);
121 g.add_edge(0, 1).unwrap();
122 g.add_edge(1, 2).unwrap();
123 g.add_edge(2, 0).unwrap();
124 assert!(is_paw_free(&g).unwrap());
125 }
126
127 #[test]
128 fn paw() {
129 // Triangle 0-1-2 + pendant 2-3
130 let mut g = Graph::with_vertices(4);
131 g.add_edge(0, 1).unwrap();
132 g.add_edge(1, 2).unwrap();
133 g.add_edge(2, 0).unwrap();
134 g.add_edge(2, 3).unwrap();
135 assert!(!is_paw_free(&g).unwrap());
136 }
137
138 #[test]
139 fn k4_paw_free() {
140 // K_4 is complete → paw-free (no vertex adjacent to exactly 1 of a triangle)
141 let mut g = Graph::with_vertices(4);
142 g.add_edge(0, 1).unwrap();
143 g.add_edge(0, 2).unwrap();
144 g.add_edge(0, 3).unwrap();
145 g.add_edge(1, 2).unwrap();
146 g.add_edge(1, 3).unwrap();
147 g.add_edge(2, 3).unwrap();
148 assert!(is_paw_free(&g).unwrap());
149 }
150
151 #[test]
152 fn path_paw_free() {
153 // Path: no triangles → no paw
154 let mut g = Graph::with_vertices(5);
155 g.add_edge(0, 1).unwrap();
156 g.add_edge(1, 2).unwrap();
157 g.add_edge(2, 3).unwrap();
158 g.add_edge(3, 4).unwrap();
159 assert!(is_paw_free(&g).unwrap());
160 }
161
162 #[test]
163 fn c5_paw_free() {
164 // C_5 is triangle-free → paw-free
165 let mut g = Graph::with_vertices(5);
166 g.add_edge(0, 1).unwrap();
167 g.add_edge(1, 2).unwrap();
168 g.add_edge(2, 3).unwrap();
169 g.add_edge(3, 4).unwrap();
170 g.add_edge(4, 0).unwrap();
171 assert!(is_paw_free(&g).unwrap());
172 }
173
174 #[test]
175 fn diamond_not_paw_free() {
176 // Diamond: K_4 minus edge 2-3. Triangle {0,1,2} with vertex 3
177 // adjacent to 0 and 1 but not 2 → vertex 3 adj to 2 of triangle
178 // Actually: diamond = {01, 02, 03, 12, 13}. Triangle {0,1,2}.
179 // Vertex 3 is adjacent to 0 and 1 (count=2) → not exactly 1.
180 // Triangle {0,1,3}: vertex 2 adjacent to 0 and 1 (count=2) → not 1.
181 // So diamond might actually be paw-free? Let me check:
182 // Triangle {0,1,2}: vertex 3 adj to 0(yes) 1(yes) 2(no) → count=2
183 // Triangle {0,1,3}: vertex 2 adj to 0(yes) 1(yes) 3(no) → count=2
184 // No triangle has a vertex adjacent to exactly 1 member → paw-free!
185 let mut g = Graph::with_vertices(4);
186 g.add_edge(0, 1).unwrap();
187 g.add_edge(0, 2).unwrap();
188 g.add_edge(0, 3).unwrap();
189 g.add_edge(1, 2).unwrap();
190 g.add_edge(1, 3).unwrap();
191 assert!(is_paw_free(&g).unwrap());
192 }
193
194 #[test]
195 fn bull_not_paw_free() {
196 // Bull: triangle 0-1-2, pendants 1-3 and 2-4
197 // Triangle {0,1,2}, vertex 3 adjacent to 1 only → count=1 → paw!
198 let mut g = Graph::with_vertices(5);
199 g.add_edge(0, 1).unwrap();
200 g.add_edge(0, 2).unwrap();
201 g.add_edge(1, 2).unwrap();
202 g.add_edge(1, 3).unwrap();
203 g.add_edge(2, 4).unwrap();
204 assert!(!is_paw_free(&g).unwrap());
205 }
206
207 #[test]
208 fn disconnected_triangle_plus_edge() {
209 // Triangle + isolated edge: no paw (the edge vertex is in
210 // a different component from the triangle)
211 let mut g = Graph::with_vertices(5);
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(3, 4).unwrap();
216 // Triangle {0,1,2}: vertex 3 adj to 0(no) 1(no) 2(no) → count=0
217 // vertex 4 adj to 0(no) 1(no) 2(no) → count=0
218 // No paw!
219 assert!(is_paw_free(&g).unwrap());
220 }
221
222 #[test]
223 fn directed_returns_false() {
224 let mut g = Graph::new(3, true).unwrap();
225 g.add_edge(0, 1).unwrap();
226 g.add_edge(1, 2).unwrap();
227 g.add_edge(2, 0).unwrap();
228 assert!(!is_paw_free(&g).unwrap());
229 }
230
231 #[test]
232 fn isolated_vertices() {
233 let g = Graph::with_vertices(10);
234 assert!(is_paw_free(&g).unwrap());
235 }
236
237 #[test]
238 fn windmill_not_paw_free() {
239 // Wd(3,2): two triangles sharing vertex 0
240 // 0-1-2-0, 0-3-4-0
241 // Triangle {0,1,2}: vertex 3 adj to 0 only → count=1 → paw!
242 let mut g = Graph::with_vertices(5);
243 g.add_edge(0, 1).unwrap();
244 g.add_edge(0, 2).unwrap();
245 g.add_edge(1, 2).unwrap();
246 g.add_edge(0, 3).unwrap();
247 g.add_edge(0, 4).unwrap();
248 g.add_edge(3, 4).unwrap();
249 assert!(!is_paw_free(&g).unwrap());
250 }
251}