Skip to main content

rust_igraph/algorithms/properties/
is_wheel.rs

1//! Wheel graph predicate (ALGO-PR-085).
2//!
3//! A wheel graph `W_n` (n ≥ 4 vertices) consists of a single hub
4//! vertex adjacent to all other vertices, which themselves form a
5//! cycle. Equivalently: one vertex of degree n-1, all other vertices
6//! of degree 3, exactly 2(n-1) edges, and the graph is simple and
7//! undirected.
8//!
9//! For directed graphs, the function returns `false`.
10
11use crate::algorithms::properties::is_simple::is_simple;
12use crate::core::{Graph, IgraphResult};
13
14/// Check whether a graph is a wheel graph.
15///
16/// A wheel graph has n ≥ 4 vertices: one hub of degree n-1 connected
17/// to all others, and the remaining n-1 vertices form a cycle (each
18/// has degree 3).
19///
20/// Returns `false` for directed graphs, non-simple graphs, and graphs
21/// with fewer than 4 vertices.
22///
23/// # Examples
24///
25/// ```
26/// use rust_igraph::{Graph, is_wheel};
27///
28/// // W4: hub 0 + triangle 1-2-3
29/// let mut g = Graph::with_vertices(4);
30/// g.add_edge(0, 1).unwrap();
31/// g.add_edge(0, 2).unwrap();
32/// g.add_edge(0, 3).unwrap();
33/// g.add_edge(1, 2).unwrap();
34/// g.add_edge(2, 3).unwrap();
35/// g.add_edge(3, 1).unwrap();
36/// assert!(is_wheel(&g).unwrap());
37///
38/// // K4 is NOT a wheel (all vertices have degree 3, no unique hub)
39/// // Actually K4 IS W4 — every vertex can serve as the hub.
40/// // But a cycle C4 is NOT a wheel:
41/// let mut g = Graph::with_vertices(4);
42/// g.add_edge(0, 1).unwrap();
43/// g.add_edge(1, 2).unwrap();
44/// g.add_edge(2, 3).unwrap();
45/// g.add_edge(3, 0).unwrap();
46/// assert!(!is_wheel(&g).unwrap());
47/// ```
48pub fn is_wheel(graph: &Graph) -> IgraphResult<bool> {
49    if graph.is_directed() {
50        return Ok(false);
51    }
52
53    let n = graph.vcount();
54    if n < 4 {
55        return Ok(false);
56    }
57
58    if !is_simple(graph)? {
59        return Ok(false);
60    }
61
62    // A wheel on n vertices has exactly 2(n-1) edges
63    let n_usize = n as usize;
64    let expected_edges = 2 * (n_usize - 1);
65    if graph.ecount() != expected_edges {
66        return Ok(false);
67    }
68
69    // Find the hub: exactly one vertex with degree n-1
70    let mut hub: Option<u32> = None;
71    for v in 0..n {
72        let deg = graph.degree(v)?;
73        if deg == n_usize - 1 {
74            if hub.is_some() {
75                // Multiple hubs → check if it's still a valid wheel
76                // (K4 has all vertices with degree 3 = n-1, and K4 IS W4)
77                // We allow multiple candidate hubs; we just need ONE that works.
78                // We'll verify below.
79            }
80            if hub.is_none() {
81                hub = Some(v);
82            }
83        }
84    }
85
86    let Some(hub) = hub else {
87        return Ok(false);
88    };
89
90    // Verify: all non-hub vertices have degree 3
91    for v in 0..n {
92        if v == hub {
93            continue;
94        }
95        if graph.degree(v)? != 3 {
96            return Ok(false);
97        }
98    }
99
100    // Verify: the non-hub vertices form a cycle.
101    // Each non-hub vertex has 3 neighbors: the hub + 2 rim neighbors.
102    // Walk the rim: start from any non-hub, follow rim edges (non-hub neighbors).
103    let rim_start = u32::from(hub == 0);
104    let rim_size = n - 1;
105
106    let mut visited = vec![false; n as usize];
107    visited[hub as usize] = true;
108    visited[rim_start as usize] = true;
109
110    // First step: rim_start has exactly 2 rim neighbors; pick the first.
111    let first_nbrs = graph.neighbors(rim_start)?;
112    let first_rim: Vec<u32> = first_nbrs.into_iter().filter(|&w| w != hub).collect();
113    if first_rim.len() != 2 {
114        return Ok(false);
115    }
116
117    let mut current = first_rim[0];
118    visited[current as usize] = true;
119    let mut count: u32 = 2;
120
121    while count < rim_size {
122        let nbrs = graph.neighbors(current)?;
123        let rim_nbrs: Vec<u32> = nbrs
124            .into_iter()
125            .filter(|&w| w != hub && !visited[w as usize])
126            .collect();
127
128        if rim_nbrs.len() != 1 {
129            return Ok(false);
130        }
131
132        current = rim_nbrs[0];
133        visited[current as usize] = true;
134        count = count.saturating_add(1);
135    }
136
137    // All rim vertices visited; the last must connect back to rim_start.
138    let last_nbrs = graph.neighbors(current)?;
139    Ok(last_nbrs.contains(&rim_start))
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn empty_graph() {
148        let g = Graph::with_vertices(0);
149        assert!(!is_wheel(&g).unwrap());
150    }
151
152    #[test]
153    fn three_vertices_too_small() {
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_wheel(&g).unwrap());
159    }
160
161    #[test]
162    fn w4_hub_0() {
163        // Hub 0 connected to 1,2,3; rim: 1-2-3-1
164        let mut g = Graph::with_vertices(4);
165        g.add_edge(0, 1).unwrap();
166        g.add_edge(0, 2).unwrap();
167        g.add_edge(0, 3).unwrap();
168        g.add_edge(1, 2).unwrap();
169        g.add_edge(2, 3).unwrap();
170        g.add_edge(3, 1).unwrap();
171        assert!(is_wheel(&g).unwrap());
172    }
173
174    #[test]
175    fn w5() {
176        // Hub 0 connected to 1,2,3,4; rim: 1-2-3-4-1
177        let mut g = Graph::with_vertices(5);
178        g.add_edge(0, 1).unwrap();
179        g.add_edge(0, 2).unwrap();
180        g.add_edge(0, 3).unwrap();
181        g.add_edge(0, 4).unwrap();
182        g.add_edge(1, 2).unwrap();
183        g.add_edge(2, 3).unwrap();
184        g.add_edge(3, 4).unwrap();
185        g.add_edge(4, 1).unwrap();
186        assert!(is_wheel(&g).unwrap());
187    }
188
189    #[test]
190    fn w6() {
191        // Hub 0, rim: 1-2-3-4-5-1
192        let mut g = Graph::with_vertices(6);
193        for i in 1..6u32 {
194            g.add_edge(0, i).unwrap();
195        }
196        g.add_edge(1, 2).unwrap();
197        g.add_edge(2, 3).unwrap();
198        g.add_edge(3, 4).unwrap();
199        g.add_edge(4, 5).unwrap();
200        g.add_edge(5, 1).unwrap();
201        assert!(is_wheel(&g).unwrap());
202    }
203
204    #[test]
205    fn hub_not_vertex_0() {
206        // Hub is vertex 3, rim: 0-1-2-0
207        let mut g = Graph::with_vertices(4);
208        g.add_edge(3, 0).unwrap();
209        g.add_edge(3, 1).unwrap();
210        g.add_edge(3, 2).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_wheel(&g).unwrap());
215    }
216
217    #[test]
218    fn c4_not_wheel() {
219        // C4: no hub vertex with degree n-1
220        let mut g = Graph::with_vertices(4);
221        g.add_edge(0, 1).unwrap();
222        g.add_edge(1, 2).unwrap();
223        g.add_edge(2, 3).unwrap();
224        g.add_edge(3, 0).unwrap();
225        assert!(!is_wheel(&g).unwrap());
226    }
227
228    #[test]
229    fn star_not_wheel() {
230        // Star K_{1,4}: hub has degree 4, leaves have degree 1 (not 3)
231        let mut g = Graph::with_vertices(5);
232        for i in 1..5u32 {
233            g.add_edge(0, i).unwrap();
234        }
235        assert!(!is_wheel(&g).unwrap());
236    }
237
238    #[test]
239    fn directed_returns_false() {
240        let mut g = Graph::new(4, true).unwrap();
241        g.add_edge(0, 1).unwrap();
242        g.add_edge(0, 2).unwrap();
243        g.add_edge(0, 3).unwrap();
244        g.add_edge(1, 2).unwrap();
245        g.add_edge(2, 3).unwrap();
246        g.add_edge(3, 1).unwrap();
247        assert!(!is_wheel(&g).unwrap());
248    }
249
250    #[test]
251    fn k4_is_wheel() {
252        // K4 is isomorphic to W4 (any vertex can be the hub)
253        let mut g = Graph::with_vertices(4);
254        for u in 0..4u32 {
255            for v in (u + 1)..4 {
256                g.add_edge(u, v).unwrap();
257            }
258        }
259        assert!(is_wheel(&g).unwrap());
260    }
261
262    #[test]
263    fn k5_not_wheel() {
264        // K5: 10 edges, but W5 has 8 edges → wrong edge count
265        let mut g = Graph::with_vertices(5);
266        for u in 0..5u32 {
267            for v in (u + 1)..5 {
268                g.add_edge(u, v).unwrap();
269            }
270        }
271        assert!(!is_wheel(&g).unwrap());
272    }
273
274    #[test]
275    fn broken_rim_not_wheel() {
276        // Hub 0 + rim 1,2,3,4 but rim has wrong connectivity
277        // (1-2, 2-3, 3-4, but 4-1 missing, 1-3 added instead)
278        let mut g = Graph::with_vertices(5);
279        g.add_edge(0, 1).unwrap();
280        g.add_edge(0, 2).unwrap();
281        g.add_edge(0, 3).unwrap();
282        g.add_edge(0, 4).unwrap();
283        g.add_edge(1, 2).unwrap();
284        g.add_edge(2, 3).unwrap();
285        g.add_edge(3, 4).unwrap();
286        g.add_edge(1, 3).unwrap();
287        assert!(!is_wheel(&g).unwrap());
288    }
289
290    #[test]
291    fn petersen_not_wheel() {
292        // Petersen: 3-regular, 10 vertices, no vertex has degree 9
293        let mut g = Graph::with_vertices(10);
294        g.add_edge(0, 1).unwrap();
295        g.add_edge(1, 2).unwrap();
296        g.add_edge(2, 3).unwrap();
297        g.add_edge(3, 4).unwrap();
298        g.add_edge(4, 0).unwrap();
299        g.add_edge(5, 7).unwrap();
300        g.add_edge(7, 9).unwrap();
301        g.add_edge(9, 6).unwrap();
302        g.add_edge(6, 8).unwrap();
303        g.add_edge(8, 5).unwrap();
304        g.add_edge(0, 5).unwrap();
305        g.add_edge(1, 6).unwrap();
306        g.add_edge(2, 7).unwrap();
307        g.add_edge(3, 8).unwrap();
308        g.add_edge(4, 9).unwrap();
309        assert!(!is_wheel(&g).unwrap());
310    }
311}