Skip to main content

rust_igraph/algorithms/properties/
is_series_parallel.rs

1//! Series-parallel graph predicate (ALGO-PR-127).
2//!
3//! A graph is series-parallel if it can be reduced to a single edge
4//! (or the empty graph) by repeated application of two operations:
5//!
6//! 1. **Series reduction**: remove a degree-2 vertex and merge its
7//!    two incident edges into one.
8//! 2. **Parallel reduction**: collapse a pair of parallel edges into
9//!    a single edge.
10//!
11//! Equivalently, a graph is series-parallel if and only if it has no
12//! `K_4` minor (Duffin 1965).
13//!
14//! The algorithm works by iteratively peeling degree-0 and degree-1
15//! vertices (trivially removable), then series-reducing degree-2
16//! vertices, and collapsing parallel edges, until no more reductions
17//! apply. If the graph reduces to at most one edge, it is SP.
18//!
19//! Directed graphs are treated as undirected.
20
21use crate::core::{Graph, IgraphResult};
22
23/// Check whether a graph is series-parallel.
24///
25/// A graph is series-parallel if it has no `K_4` minor. The test
26/// works by iterative series and parallel reductions.
27///
28/// Directed graphs are treated as undirected.
29///
30/// # Examples
31///
32/// ```
33/// use rust_igraph::{Graph, is_series_parallel};
34///
35/// // Path: trivially series-parallel
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, 3).unwrap();
40/// assert!(is_series_parallel(&g).unwrap());
41///
42/// // K_4: NOT series-parallel (has K_4 minor)
43/// let mut g = Graph::with_vertices(4);
44/// for i in 0..4u32 {
45///     for j in (i+1)..4 {
46///         g.add_edge(i, j).unwrap();
47///     }
48/// }
49/// assert!(!is_series_parallel(&g).unwrap());
50/// ```
51pub fn is_series_parallel(graph: &Graph) -> IgraphResult<bool> {
52    let n = graph.vcount() as usize;
53    if n == 0 {
54        return Ok(true);
55    }
56
57    let mut adj: Vec<Vec<usize>> = vec![vec![]; n];
58    for v in 0..graph.vcount() {
59        let nbrs = graph.neighbors(v)?;
60        for &w in &nbrs {
61            let ww = w as usize;
62            let vv = v as usize;
63            if vv < ww {
64                adj[vv].push(ww);
65                adj[ww].push(vv);
66            }
67        }
68    }
69
70    Ok(sp_reduce(&mut adj, n))
71}
72
73fn sp_reduce(adj: &mut [Vec<usize>], n: usize) -> bool {
74    let mut alive = vec![true; n];
75    let mut changed = true;
76
77    while changed {
78        changed = false;
79
80        // Parallel reduction: for each alive vertex, collapse duplicate edges
81        for v in 0..n {
82            if !alive[v] {
83                continue;
84            }
85            adj[v].sort_unstable();
86            let before = adj[v].len();
87            adj[v].dedup();
88            if adj[v].len() < before {
89                changed = true;
90                // Also dedup the reverse direction for neighbors
91                let nbrs: Vec<usize> = adj[v].clone();
92                for w in nbrs {
93                    adj[w].sort_unstable();
94                    adj[w].dedup();
95                }
96            }
97        }
98
99        for v in 0..n {
100            if !alive[v] {
101                continue;
102            }
103
104            let deg = adj[v].len();
105
106            if deg == 0 {
107                alive[v] = false;
108                changed = true;
109                continue;
110            }
111
112            if deg == 1 {
113                let w = adj[v][0];
114                adj[v].clear();
115                adj[w].retain(|&x| x != v);
116                alive[v] = false;
117                changed = true;
118                continue;
119            }
120
121            if deg == 2 && adj[v][0] != adj[v][1] {
122                let a = adj[v][0];
123                let b = adj[v][1];
124                adj[v].clear();
125                alive[v] = false;
126
127                adj[a].retain(|&x| x != v);
128                adj[b].retain(|&x| x != v);
129                adj[a].push(b);
130                adj[b].push(a);
131                changed = true;
132            }
133        }
134    }
135
136    let remaining_edges: usize = adj.iter().map(Vec::len).sum::<usize>() / 2;
137    let remaining_verts = alive.iter().filter(|&&a| a).count();
138
139    remaining_verts <= 2 && remaining_edges <= 1
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_series_parallel(&g).unwrap());
150    }
151
152    #[test]
153    fn single_vertex() {
154        let g = Graph::with_vertices(1);
155        assert!(is_series_parallel(&g).unwrap());
156    }
157
158    #[test]
159    fn single_edge() {
160        let mut g = Graph::with_vertices(2);
161        g.add_edge(0, 1).unwrap();
162        assert!(is_series_parallel(&g).unwrap());
163    }
164
165    #[test]
166    fn triangle() {
167        let mut g = Graph::with_vertices(3);
168        g.add_edge(0, 1).unwrap();
169        g.add_edge(1, 2).unwrap();
170        g.add_edge(2, 0).unwrap();
171        assert!(is_series_parallel(&g).unwrap());
172    }
173
174    #[test]
175    fn path() {
176        let mut g = Graph::with_vertices(5);
177        g.add_edge(0, 1).unwrap();
178        g.add_edge(1, 2).unwrap();
179        g.add_edge(2, 3).unwrap();
180        g.add_edge(3, 4).unwrap();
181        assert!(is_series_parallel(&g).unwrap());
182    }
183
184    #[test]
185    fn cycle_c4() {
186        let mut g = Graph::with_vertices(4);
187        g.add_edge(0, 1).unwrap();
188        g.add_edge(1, 2).unwrap();
189        g.add_edge(2, 3).unwrap();
190        g.add_edge(3, 0).unwrap();
191        assert!(is_series_parallel(&g).unwrap());
192    }
193
194    #[test]
195    fn cycle_c5() {
196        let mut g = Graph::with_vertices(5);
197        g.add_edge(0, 1).unwrap();
198        g.add_edge(1, 2).unwrap();
199        g.add_edge(2, 3).unwrap();
200        g.add_edge(3, 4).unwrap();
201        g.add_edge(4, 0).unwrap();
202        assert!(is_series_parallel(&g).unwrap());
203    }
204
205    #[test]
206    fn k4_not_sp() {
207        let mut g = Graph::with_vertices(4);
208        for i in 0..4u32 {
209            for j in (i + 1)..4 {
210                g.add_edge(i, j).unwrap();
211            }
212        }
213        assert!(!is_series_parallel(&g).unwrap());
214    }
215
216    #[test]
217    fn k5_not_sp() {
218        let mut g = Graph::with_vertices(5);
219        for i in 0..5u32 {
220            for j in (i + 1)..5 {
221                g.add_edge(i, j).unwrap();
222            }
223        }
224        assert!(!is_series_parallel(&g).unwrap());
225    }
226
227    #[test]
228    fn diamond_sp() {
229        // Diamond: K_4 minus one edge. 4 vertices, 5 edges.
230        // Series-parallel because no K_4 minor.
231        let mut g = Graph::with_vertices(4);
232        g.add_edge(0, 1).unwrap();
233        g.add_edge(0, 2).unwrap();
234        g.add_edge(0, 3).unwrap();
235        g.add_edge(1, 2).unwrap();
236        g.add_edge(1, 3).unwrap();
237        // Missing edge: 2-3. Diamond has K_4 minus one edge.
238        assert!(is_series_parallel(&g).unwrap());
239    }
240
241    #[test]
242    fn wheatstone_bridge_sp() {
243        // Wheatstone bridge: K_4 minus one edge with subdivided edges
244        // This is a classic SP graph
245        // 0-1, 0-2, 1-3, 2-3, 1-2 (5 edges, 4 vertices)
246        // Same as diamond — SP
247        let mut g = Graph::with_vertices(4);
248        g.add_edge(0, 1).unwrap();
249        g.add_edge(0, 2).unwrap();
250        g.add_edge(1, 2).unwrap();
251        g.add_edge(1, 3).unwrap();
252        g.add_edge(2, 3).unwrap();
253        assert!(is_series_parallel(&g).unwrap());
254    }
255
256    #[test]
257    fn tree_sp() {
258        // Any tree is SP
259        let mut g = Graph::with_vertices(6);
260        g.add_edge(0, 1).unwrap();
261        g.add_edge(1, 2).unwrap();
262        g.add_edge(1, 3).unwrap();
263        g.add_edge(3, 4).unwrap();
264        g.add_edge(3, 5).unwrap();
265        assert!(is_series_parallel(&g).unwrap());
266    }
267
268    #[test]
269    fn edgeless() {
270        let g = Graph::with_vertices(5);
271        assert!(is_series_parallel(&g).unwrap());
272    }
273
274    #[test]
275    fn star() {
276        let mut g = Graph::with_vertices(5);
277        g.add_edge(0, 1).unwrap();
278        g.add_edge(0, 2).unwrap();
279        g.add_edge(0, 3).unwrap();
280        g.add_edge(0, 4).unwrap();
281        assert!(is_series_parallel(&g).unwrap());
282    }
283
284    #[test]
285    fn petersen_not_sp() {
286        // Petersen graph contains K_4 minor → not SP
287        let mut g = Graph::with_vertices(10);
288        let edges = [
289            (0, 1),
290            (1, 2),
291            (2, 3),
292            (3, 4),
293            (4, 0),
294            (5, 7),
295            (7, 9),
296            (9, 6),
297            (6, 8),
298            (8, 5),
299            (0, 5),
300            (1, 6),
301            (2, 7),
302            (3, 8),
303            (4, 9),
304        ];
305        for (u, v) in edges {
306            g.add_edge(u, v).unwrap();
307        }
308        assert!(!is_series_parallel(&g).unwrap());
309    }
310
311    #[test]
312    fn k23_sp() {
313        // K_{2,3}: 2 + 3 vertices, 6 edges. No K_4 minor (bipartite with
314        // max partition size 3, but K_4 has min degree 3 which a side-2
315        // bipartite graph can't support). SP.
316        let mut g = Graph::with_vertices(5);
317        g.add_edge(0, 2).unwrap();
318        g.add_edge(0, 3).unwrap();
319        g.add_edge(0, 4).unwrap();
320        g.add_edge(1, 2).unwrap();
321        g.add_edge(1, 3).unwrap();
322        g.add_edge(1, 4).unwrap();
323        assert!(is_series_parallel(&g).unwrap());
324    }
325
326    #[test]
327    fn two_triangles_shared_edge() {
328        // 0-1-2-0 and 1-2-3-1 share edge 1-2.
329        // 4 vertices, 5 edges. Diamond shape. SP.
330        let mut g = Graph::with_vertices(4);
331        g.add_edge(0, 1).unwrap();
332        g.add_edge(0, 2).unwrap();
333        g.add_edge(1, 2).unwrap();
334        g.add_edge(1, 3).unwrap();
335        g.add_edge(2, 3).unwrap();
336        assert!(is_series_parallel(&g).unwrap());
337    }
338
339    #[test]
340    fn directed_treated_as_undirected() {
341        let mut g = Graph::new(4, true).unwrap();
342        for i in 0..4u32 {
343            for j in (i + 1)..4 {
344                g.add_edge(i, j).unwrap();
345            }
346        }
347        // K_4 directed → treated as undirected → not SP
348        assert!(!is_series_parallel(&g).unwrap());
349    }
350
351    #[test]
352    fn disconnected_sp() {
353        // Two separate triangles: each is SP, and SP is closed under disjoint union
354        let mut g = Graph::with_vertices(6);
355        g.add_edge(0, 1).unwrap();
356        g.add_edge(1, 2).unwrap();
357        g.add_edge(2, 0).unwrap();
358        g.add_edge(3, 4).unwrap();
359        g.add_edge(4, 5).unwrap();
360        g.add_edge(5, 3).unwrap();
361        assert!(is_series_parallel(&g).unwrap());
362    }
363
364    #[test]
365    fn wheel_w4_not_sp() {
366        // W_4: center 0, rim 1-2-3-4-1. Contains K_4 minor (contract rim edge).
367        let mut g = Graph::with_vertices(5);
368        g.add_edge(0, 1).unwrap();
369        g.add_edge(0, 2).unwrap();
370        g.add_edge(0, 3).unwrap();
371        g.add_edge(0, 4).unwrap();
372        g.add_edge(1, 2).unwrap();
373        g.add_edge(2, 3).unwrap();
374        g.add_edge(3, 4).unwrap();
375        g.add_edge(4, 1).unwrap();
376        assert!(!is_series_parallel(&g).unwrap());
377    }
378}