Skip to main content

rust_igraph/algorithms/properties/
is_strongly_regular.rs

1//! Strongly regular graph predicate (ALGO-PR-082).
2//!
3//! A graph is strongly regular with parameters (n, k, λ, μ) if:
4//! - It has n vertices and is k-regular,
5//! - Every pair of adjacent vertices has exactly λ common neighbors,
6//! - Every pair of non-adjacent vertices has exactly μ common neighbors.
7//!
8//! Complete graphs `K_n` are trivially strongly regular (μ is undefined).
9//! Edgeless graphs are trivially strongly regular (λ is undefined).
10//! For non-trivial strongly regular graphs, 0 < k < n-1.
11//!
12//! For directed graphs, the function returns `false`.
13//! For graphs with self-loops or multi-edges, the function returns `false`.
14
15use crate::algorithms::properties::is_simple::is_simple;
16use crate::core::{Graph, IgraphResult};
17
18/// Result of strongly regular graph recognition.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct StronglyRegularParams {
21    /// Number of vertices.
22    pub n: u32,
23    /// Degree of each vertex.
24    pub k: u32,
25    /// Number of common neighbors for each pair of adjacent vertices.
26    pub lambda: u32,
27    /// Number of common neighbors for each pair of non-adjacent vertices.
28    pub mu: u32,
29}
30
31/// Check whether a graph is strongly regular.
32///
33/// Returns `Some(params)` if the graph is strongly regular, `None`
34/// otherwise.
35///
36/// Returns `None` for directed graphs and non-simple graphs.
37///
38/// Complete graphs and edgeless graphs are considered strongly
39/// regular (with degenerate parameters).
40///
41/// # Examples
42///
43/// ```
44/// use rust_igraph::{Graph, is_strongly_regular};
45///
46/// // Petersen graph: strongly regular (10, 3, 0, 1)
47/// let mut g = Graph::with_vertices(10);
48/// // Outer cycle
49/// g.add_edge(0, 1).unwrap();
50/// g.add_edge(1, 2).unwrap();
51/// g.add_edge(2, 3).unwrap();
52/// g.add_edge(3, 4).unwrap();
53/// g.add_edge(4, 0).unwrap();
54/// // Inner pentagram
55/// g.add_edge(5, 7).unwrap();
56/// g.add_edge(7, 9).unwrap();
57/// g.add_edge(9, 6).unwrap();
58/// g.add_edge(6, 8).unwrap();
59/// g.add_edge(8, 5).unwrap();
60/// // Spokes
61/// g.add_edge(0, 5).unwrap();
62/// g.add_edge(1, 6).unwrap();
63/// g.add_edge(2, 7).unwrap();
64/// g.add_edge(3, 8).unwrap();
65/// g.add_edge(4, 9).unwrap();
66/// let params = is_strongly_regular(&g).unwrap().unwrap();
67/// assert_eq!(params.n, 10);
68/// assert_eq!(params.k, 3);
69/// assert_eq!(params.lambda, 0);
70/// assert_eq!(params.mu, 1);
71///
72/// // Path P4 is NOT strongly regular (not regular)
73/// let mut g = Graph::with_vertices(4);
74/// g.add_edge(0, 1).unwrap();
75/// g.add_edge(1, 2).unwrap();
76/// g.add_edge(2, 3).unwrap();
77/// assert!(is_strongly_regular(&g).unwrap().is_none());
78/// ```
79pub fn is_strongly_regular(graph: &Graph) -> IgraphResult<Option<StronglyRegularParams>> {
80    if graph.is_directed() {
81        return Ok(None);
82    }
83
84    let n = graph.vcount();
85    if n == 0 {
86        return Ok(Some(StronglyRegularParams {
87            n: 0,
88            k: 0,
89            lambda: 0,
90            mu: 0,
91        }));
92    }
93
94    if !is_simple(graph)? {
95        return Ok(None);
96    }
97
98    // Check regularity
99    let deg0 = graph.neighbors(0)?.len();
100    for v in 1..n {
101        let dv = graph.neighbors(v)?.len();
102        if dv != deg0 {
103            return Ok(None);
104        }
105    }
106    let k = u32::try_from(deg0).unwrap_or(u32::MAX);
107
108    // Degenerate cases
109    if k == 0 {
110        // Edgeless graph: strongly regular with λ=0, μ=0
111        return Ok(Some(StronglyRegularParams {
112            n,
113            k: 0,
114            lambda: 0,
115            mu: 0,
116        }));
117    }
118    if k == n.saturating_sub(1) {
119        // Complete graph: strongly regular with λ=n-2, μ=0 (no non-adj pairs)
120        return Ok(Some(StronglyRegularParams {
121            n,
122            k,
123            lambda: n.saturating_sub(2),
124            mu: 0,
125        }));
126    }
127
128    // Build adjacency sets for O(1) lookup
129    let n_usize = n as usize;
130    let mut adj = vec![vec![false; n_usize]; n_usize];
131    for v in 0..n {
132        let nbrs = graph.neighbors(v)?;
133        for w in nbrs {
134            adj[v as usize][w as usize] = true;
135        }
136    }
137
138    // Check λ and μ parameters
139    let mut lambda: Option<u32> = None;
140    let mut mu: Option<u32> = None;
141
142    for u in 0..n {
143        for v in (u + 1)..n {
144            let common = count_common_neighbors(&adj, u as usize, v as usize, n_usize);
145
146            if adj[u as usize][v as usize] {
147                // Adjacent pair
148                match lambda {
149                    None => lambda = Some(common),
150                    Some(l) if l != common => return Ok(None),
151                    _ => {}
152                }
153            } else {
154                // Non-adjacent pair
155                match mu {
156                    None => mu = Some(common),
157                    Some(m) if m != common => return Ok(None),
158                    _ => {}
159                }
160            }
161        }
162    }
163
164    Ok(Some(StronglyRegularParams {
165        n,
166        k,
167        lambda: lambda.unwrap_or(0),
168        mu: mu.unwrap_or(0),
169    }))
170}
171
172fn count_common_neighbors(adj: &[Vec<bool>], u: usize, v: usize, n: usize) -> u32 {
173    let mut count = 0u32;
174    for (au, av) in adj[u][..n].iter().zip(adj[v][..n].iter()) {
175        if *au && *av {
176            count = count.saturating_add(1);
177        }
178    }
179    count
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn empty_graph() {
188        let g = Graph::with_vertices(0);
189        let params = is_strongly_regular(&g).unwrap().unwrap();
190        assert_eq!(params.n, 0);
191    }
192
193    #[test]
194    fn single_vertex() {
195        let g = Graph::with_vertices(1);
196        let params = is_strongly_regular(&g).unwrap().unwrap();
197        assert_eq!(params.n, 1);
198        assert_eq!(params.k, 0);
199    }
200
201    #[test]
202    fn single_edge() {
203        let mut g = Graph::with_vertices(2);
204        g.add_edge(0, 1).unwrap();
205        let params = is_strongly_regular(&g).unwrap().unwrap();
206        assert_eq!(params.n, 2);
207        assert_eq!(params.k, 1);
208    }
209
210    #[test]
211    fn triangle() {
212        let mut g = Graph::with_vertices(3);
213        g.add_edge(0, 1).unwrap();
214        g.add_edge(1, 2).unwrap();
215        g.add_edge(2, 0).unwrap();
216        let params = is_strongly_regular(&g).unwrap().unwrap();
217        assert_eq!(params.n, 3);
218        assert_eq!(params.k, 2);
219        assert_eq!(params.lambda, 1);
220    }
221
222    #[test]
223    fn k4() {
224        let mut g = Graph::with_vertices(4);
225        for u in 0..4u32 {
226            for v in (u + 1)..4 {
227                g.add_edge(u, v).unwrap();
228            }
229        }
230        let params = is_strongly_regular(&g).unwrap().unwrap();
231        assert_eq!(params.n, 4);
232        assert_eq!(params.k, 3);
233        assert_eq!(params.lambda, 2);
234    }
235
236    #[test]
237    fn edgeless() {
238        let g = Graph::with_vertices(5);
239        let params = is_strongly_regular(&g).unwrap().unwrap();
240        assert_eq!(params.n, 5);
241        assert_eq!(params.k, 0);
242    }
243
244    #[test]
245    fn path_not_sr() {
246        let mut g = Graph::with_vertices(4);
247        g.add_edge(0, 1).unwrap();
248        g.add_edge(1, 2).unwrap();
249        g.add_edge(2, 3).unwrap();
250        assert!(is_strongly_regular(&g).unwrap().is_none());
251    }
252
253    #[test]
254    fn c5_is_sr() {
255        // C5 is strongly regular (5, 2, 0, 1)
256        let mut g = Graph::with_vertices(5);
257        g.add_edge(0, 1).unwrap();
258        g.add_edge(1, 2).unwrap();
259        g.add_edge(2, 3).unwrap();
260        g.add_edge(3, 4).unwrap();
261        g.add_edge(4, 0).unwrap();
262        let params = is_strongly_regular(&g).unwrap().unwrap();
263        assert_eq!(params.n, 5);
264        assert_eq!(params.k, 2);
265        assert_eq!(params.lambda, 0);
266        assert_eq!(params.mu, 1);
267    }
268
269    #[test]
270    fn c4_not_sr() {
271        // C4: regular but not strongly regular
272        // adj pairs: common nbrs differ (0-1 share {3} if adj... wait)
273        // C4: 0-1, 1-2, 2-3, 3-0
274        // 0 adj 1: common = |{3}∩{2}| = 0
275        // 0 adj 3: common = |{1}∩{2}| = 0
276        // non-adj 0,2: common = |{1,3}∩{1,3}| = 2
277        // non-adj 1,3: common = |{0,2}∩{0,2}| = 2
278        // λ=0, μ=2. k=2, n=4. Check: k(k-1-λ) = 2(2-1-0) = 2, μ(n-k-1) = 2(4-2-1) = 2. Equal! So C4 IS sr(4,2,0,2).
279        let mut g = Graph::with_vertices(4);
280        g.add_edge(0, 1).unwrap();
281        g.add_edge(1, 2).unwrap();
282        g.add_edge(2, 3).unwrap();
283        g.add_edge(3, 0).unwrap();
284        let params = is_strongly_regular(&g).unwrap().unwrap();
285        assert_eq!(params.n, 4);
286        assert_eq!(params.k, 2);
287        assert_eq!(params.lambda, 0);
288        assert_eq!(params.mu, 2);
289    }
290
291    #[test]
292    fn petersen() {
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        let params = is_strongly_regular(&g).unwrap().unwrap();
310        assert_eq!(params.n, 10);
311        assert_eq!(params.k, 3);
312        assert_eq!(params.lambda, 0);
313        assert_eq!(params.mu, 1);
314    }
315
316    #[test]
317    fn star_not_sr() {
318        let mut g = Graph::with_vertices(5);
319        for i in 1..5u32 {
320            g.add_edge(0, i).unwrap();
321        }
322        assert!(is_strongly_regular(&g).unwrap().is_none());
323    }
324
325    #[test]
326    fn c6_not_sr() {
327        // C6: 2-regular
328        // adj pair 0-1: common = |{5}∩{2}| = 0
329        // non-adj 0-2: common = |{1,5}∩{1,3}| = 1
330        // non-adj 0-3: common = |{1,5}∩{2,4}| = 0
331        // μ not constant → not strongly regular
332        let mut g = Graph::with_vertices(6);
333        g.add_edge(0, 1).unwrap();
334        g.add_edge(1, 2).unwrap();
335        g.add_edge(2, 3).unwrap();
336        g.add_edge(3, 4).unwrap();
337        g.add_edge(4, 5).unwrap();
338        g.add_edge(5, 0).unwrap();
339        assert!(is_strongly_regular(&g).unwrap().is_none());
340    }
341
342    #[test]
343    fn directed_returns_none() {
344        let mut g = Graph::new(3, true).unwrap();
345        g.add_edge(0, 1).unwrap();
346        g.add_edge(1, 2).unwrap();
347        g.add_edge(2, 0).unwrap();
348        assert!(is_strongly_regular(&g).unwrap().is_none());
349    }
350
351    #[test]
352    fn paley5_is_sr() {
353        // Paley(5) = C5, already tested
354        // Let's test complement of Petersen = Kneser(5,2)
355        // which is also strongly regular (10, 6, 3, 4)
356        let mut g = Graph::with_vertices(10);
357        g.add_edge(0, 1).unwrap();
358        g.add_edge(1, 2).unwrap();
359        g.add_edge(2, 3).unwrap();
360        g.add_edge(3, 4).unwrap();
361        g.add_edge(4, 0).unwrap();
362        g.add_edge(5, 7).unwrap();
363        g.add_edge(7, 9).unwrap();
364        g.add_edge(9, 6).unwrap();
365        g.add_edge(6, 8).unwrap();
366        g.add_edge(8, 5).unwrap();
367        g.add_edge(0, 5).unwrap();
368        g.add_edge(1, 6).unwrap();
369        g.add_edge(2, 7).unwrap();
370        g.add_edge(3, 8).unwrap();
371        g.add_edge(4, 9).unwrap();
372        let comp = crate::algorithms::operators::complementer::complementer(&g, false).unwrap();
373        let params = is_strongly_regular(&comp).unwrap().unwrap();
374        assert_eq!(params.n, 10);
375        assert_eq!(params.k, 6);
376        assert_eq!(params.lambda, 3);
377        assert_eq!(params.mu, 4);
378    }
379
380    #[test]
381    fn two_k3_is_sr() {
382        // Two disjoint K3: 2-regular, λ=1 (adj pairs share 1 common nbr),
383        // μ=0 (cross-component pairs share 0). sr(6, 2, 1, 0).
384        let mut g = Graph::with_vertices(6);
385        g.add_edge(0, 1).unwrap();
386        g.add_edge(1, 2).unwrap();
387        g.add_edge(2, 0).unwrap();
388        g.add_edge(3, 4).unwrap();
389        g.add_edge(4, 5).unwrap();
390        g.add_edge(5, 3).unwrap();
391        let params = is_strongly_regular(&g).unwrap().unwrap();
392        assert_eq!(params.n, 6);
393        assert_eq!(params.k, 2);
394        assert_eq!(params.lambda, 1);
395        assert_eq!(params.mu, 0);
396    }
397
398    #[test]
399    fn cube_not_sr() {
400        // Cube graph (Q3): 3-regular, 8 vertices
401        // adj pair (0,1): N(0)={1,3,4}, N(1)={0,2,5}, common={}→0
402        // non-adj (0,2): N(0)={1,3,4}, N(2)={1,3,6}, common={1,3}→2
403        // non-adj (0,5): N(0)={1,3,4}, N(5)={1,4,6}, common={1,4}→2
404        // non-adj (0,6): N(0)={1,3,4}, N(6)={2,5,7}, common={}→0
405        // μ not constant (0 and 2) → not sr
406        let mut g = Graph::with_vertices(8);
407        g.add_edge(0, 1).unwrap();
408        g.add_edge(1, 2).unwrap();
409        g.add_edge(2, 3).unwrap();
410        g.add_edge(3, 0).unwrap();
411        g.add_edge(4, 5).unwrap();
412        g.add_edge(5, 6).unwrap();
413        g.add_edge(6, 7).unwrap();
414        g.add_edge(7, 4).unwrap();
415        g.add_edge(0, 4).unwrap();
416        g.add_edge(1, 5).unwrap();
417        g.add_edge(2, 6).unwrap();
418        g.add_edge(3, 7).unwrap();
419        assert!(is_strongly_regular(&g).unwrap().is_none());
420    }
421}