Skip to main content

rust_igraph/algorithms/properties/
is_split.rs

1//! Split graph predicate (ALGO-PR-072).
2//!
3//! A split graph is a graph whose vertices can be partitioned into a
4//! clique and an independent set. Recognition is polynomial via the
5//! Hammer-Simeone theorem (1977): sort the degree sequence in
6//! non-increasing order, find `m = max{i : d_i >= i-1}`, then the
7//! graph is split iff `sum(d_1..d_m) = m(m-1) + sum(d_{m+1}..d_n)`.
8//!
9//! This characterization applies to simple undirected graphs. For
10//! non-simple graphs (with self-loops or parallel edges) or directed
11//! graphs, the function returns `false`.
12
13use crate::algorithms::properties::degree::{DegreeMode, degree_sequence};
14use crate::algorithms::properties::is_simple::is_simple;
15use crate::core::{Graph, IgraphResult};
16
17/// Check whether a graph is a split graph.
18///
19/// A split graph can partition its vertices into a clique C and an
20/// independent set I. Uses the Hammer-Simeone degree-sequence
21/// characterization for O(V + E) recognition.
22///
23/// Returns `false` for directed graphs, multigraphs, or graphs with
24/// self-loops (the characterization requires simple undirected graphs).
25///
26/// An empty graph (0 vertices) is a split graph (vacuously).
27/// An edgeless graph is a split graph (C = empty, I = all vertices).
28///
29/// # Examples
30///
31/// ```
32/// use rust_igraph::{Graph, is_split_graph};
33///
34/// // Path P3 (0-1-2): split as C={1}, I={0,2}
35/// let mut g = Graph::with_vertices(3);
36/// g.add_edge(0, 1).unwrap();
37/// g.add_edge(1, 2).unwrap();
38/// assert!(is_split_graph(&g).unwrap());
39///
40/// // Cycle C5 is NOT a split graph
41/// let mut g = Graph::with_vertices(5);
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, 4).unwrap();
46/// g.add_edge(4, 0).unwrap();
47/// assert!(!is_split_graph(&g).unwrap());
48/// ```
49pub fn is_split_graph(graph: &Graph) -> IgraphResult<bool> {
50    if graph.is_directed() {
51        return Ok(false);
52    }
53
54    let n = graph.vcount() as usize;
55    if n == 0 {
56        return Ok(true);
57    }
58
59    if !is_simple(graph)? {
60        return Ok(false);
61    }
62
63    let mut degrees = degree_sequence(graph, DegreeMode::All)?;
64    degrees.sort_unstable_by(|a, b| b.cmp(a));
65
66    let m = find_m(&degrees);
67    if m == 0 {
68        return Ok(true);
69    }
70
71    let sum_top: u64 = degrees[..m].iter().map(|&d| u64::from(d)).sum();
72    let sum_bottom: u64 = degrees[m..].iter().map(|&d| u64::from(d)).sum();
73    let m64 = m as u64;
74    let target = m64
75        .saturating_mul(m64.saturating_sub(1))
76        .saturating_add(sum_bottom);
77
78    Ok(sum_top == target)
79}
80
81fn find_m(sorted_desc: &[u32]) -> usize {
82    let mut m = 0usize;
83    for (i, &d) in sorted_desc.iter().enumerate() {
84        let idx = u32::try_from(i).unwrap_or(u32::MAX);
85        if d >= idx {
86            m = i.saturating_add(1);
87        }
88    }
89    m
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn empty_graph() {
98        let g = Graph::with_vertices(0);
99        assert!(is_split_graph(&g).unwrap());
100    }
101
102    #[test]
103    fn single_vertex() {
104        let g = Graph::with_vertices(1);
105        assert!(is_split_graph(&g).unwrap());
106    }
107
108    #[test]
109    fn edgeless_graph() {
110        let g = Graph::with_vertices(5);
111        assert!(is_split_graph(&g).unwrap());
112    }
113
114    #[test]
115    fn single_edge() {
116        let mut g = Graph::with_vertices(2);
117        g.add_edge(0, 1).unwrap();
118        assert!(is_split_graph(&g).unwrap());
119    }
120
121    #[test]
122    fn path_3_is_split() {
123        let mut g = Graph::with_vertices(3);
124        g.add_edge(0, 1).unwrap();
125        g.add_edge(1, 2).unwrap();
126        assert!(is_split_graph(&g).unwrap());
127    }
128
129    #[test]
130    fn triangle_is_split() {
131        // K3: C={0,1,2}, I={}
132        let mut g = Graph::with_vertices(3);
133        g.add_edge(0, 1).unwrap();
134        g.add_edge(1, 2).unwrap();
135        g.add_edge(2, 0).unwrap();
136        assert!(is_split_graph(&g).unwrap());
137    }
138
139    #[test]
140    fn complete_k4_is_split() {
141        let mut g = Graph::with_vertices(4);
142        for u in 0..4u32 {
143            for v in (u + 1)..4 {
144                g.add_edge(u, v).unwrap();
145            }
146        }
147        assert!(is_split_graph(&g).unwrap());
148    }
149
150    #[test]
151    fn star_is_split() {
152        // Star: C={center}, I={leaves}
153        let mut g = Graph::with_vertices(5);
154        for i in 1..5u32 {
155            g.add_edge(0, i).unwrap();
156        }
157        assert!(is_split_graph(&g).unwrap());
158    }
159
160    #[test]
161    fn cycle_4_is_split() {
162        // C4 is a split graph: C={0,2}, I={1,3} (if 0-2 are adjacent)
163        // Wait: C4 = 0-1-2-3-0. Vertices {0,2} are NOT adjacent.
164        // So C4 is NOT split because neither {0,2} nor any 2-subset
165        // forms a clique that leaves an IS.
166        // Actually C4 IS a split graph: C={0,1}, I={2,3}
167        // But 0-1 is an edge, so C={0,1} is a clique.
168        // I={2,3}: 2-3 is an edge! So not an IS.
169        // C={1,2}: 1-2 is an edge. I={0,3}: 3-0 is an edge. Not IS.
170        // C4 has no split partition → it's NOT split.
171        // Verify with Hammer-Simeone: degrees [2,2,2,2], m=2
172        // (d_1=2>=0, d_2=2>=1, d_3=2>=2? yes), so m=3?
173        // Actually m = max{i : d_i >= i-1} where i is 1-indexed:
174        // i=1: d_1=2>=0 yes; i=2: d_2=2>=1 yes; i=3: d_3=2>=2 yes;
175        // i=4: d_4=2>=3? no. So m=3.
176        // sum_top = 2+2+2 = 6
177        // target = 3*2 + 2 = 8 ≠ 6 → NOT split. Correct!
178        let mut g = Graph::with_vertices(4);
179        g.add_edge(0, 1).unwrap();
180        g.add_edge(1, 2).unwrap();
181        g.add_edge(2, 3).unwrap();
182        g.add_edge(3, 0).unwrap();
183        assert!(!is_split_graph(&g).unwrap());
184    }
185
186    #[test]
187    fn cycle_5_not_split() {
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_split_graph(&g).unwrap());
195    }
196
197    #[test]
198    fn k4_minus_edge_is_split() {
199        // K4 minus one edge: e.g., all K4 edges except (2,3)
200        // degrees: [3,3,2,2], sorted desc [3,3,2,2]
201        // m: i=1: 3>=0 yes; i=2: 3>=1 yes; i=3: 2>=2 yes; i=4: 2>=3 no. m=3
202        // sum_top = 3+3+2 = 8, target = 3*2 + 2 = 8 → split!
203        let mut g = Graph::with_vertices(4);
204        g.add_edge(0, 1).unwrap();
205        g.add_edge(0, 2).unwrap();
206        g.add_edge(0, 3).unwrap();
207        g.add_edge(1, 2).unwrap();
208        g.add_edge(1, 3).unwrap();
209        // no edge (2,3)
210        assert!(is_split_graph(&g).unwrap());
211    }
212
213    #[test]
214    fn bipartite_k23_not_split() {
215        // K_{2,3}: degrees [3,3,2,2,2]
216        // m: i=1: 3>=0; i=2: 3>=1; i=3: 2>=2; i=4: 2>=3? no. m=3
217        // sum_top = 3+3+2 = 8, target = 3*2 + 2+2 = 10 ≠ 8 → not split
218        let mut g = Graph::with_vertices(5);
219        g.add_edge(0, 2).unwrap();
220        g.add_edge(0, 3).unwrap();
221        g.add_edge(0, 4).unwrap();
222        g.add_edge(1, 2).unwrap();
223        g.add_edge(1, 3).unwrap();
224        g.add_edge(1, 4).unwrap();
225        assert!(!is_split_graph(&g).unwrap());
226    }
227
228    #[test]
229    fn directed_returns_false() {
230        let mut g = Graph::new(3, true).unwrap();
231        g.add_edge(0, 1).unwrap();
232        g.add_edge(1, 2).unwrap();
233        assert!(!is_split_graph(&g).unwrap());
234    }
235
236    #[test]
237    fn self_loop_returns_false() {
238        let mut g = Graph::with_vertices(2);
239        g.add_edge(0, 0).unwrap();
240        g.add_edge(0, 1).unwrap();
241        assert!(!is_split_graph(&g).unwrap());
242    }
243
244    #[test]
245    fn parallel_edges_returns_false() {
246        let mut g = Graph::with_vertices(2);
247        g.add_edge(0, 1).unwrap();
248        g.add_edge(0, 1).unwrap();
249        assert!(!is_split_graph(&g).unwrap());
250    }
251
252    #[test]
253    fn threshold_graph_is_split() {
254        // Threshold graph: start with K1, repeatedly add isolated or
255        // dominating vertex. E.g., {0}, add 1 dominating: K2.
256        // Add 2 isolated, add 3 dominating to {0,1,2}: 3-0, 3-1, 3-2.
257        // degrees: [3,2,1,2], sorted [3,2,2,1]
258        // m: i=1: 3>=0; i=2: 2>=1; i=3: 2>=2; i=4: 1>=3? no. m=3
259        // sum_top=7, target=3*2+1=7 → split!
260        let mut g = Graph::with_vertices(4);
261        g.add_edge(0, 1).unwrap();
262        g.add_edge(3, 0).unwrap();
263        g.add_edge(3, 1).unwrap();
264        g.add_edge(3, 2).unwrap();
265        assert!(is_split_graph(&g).unwrap());
266    }
267}