Skip to main content

rust_igraph/algorithms/properties/
is_threshold.rs

1//! Threshold graph predicate (ALGO-PR-073).
2//!
3//! A threshold graph can be constructed from a single vertex by
4//! repeatedly adding either an isolated vertex (connected to no
5//! existing vertex) or a dominating vertex (connected to all existing
6//! vertices). Equivalently, a graph is threshold iff it has no
7//! induced subgraph isomorphic to P4, C4, or 2K2.
8//!
9//! Recognition is O(V log V) via the degree sequence: sort degrees in
10//! non-increasing order, then repeatedly "peel" vertices. A vertex of
11//! degree n-1 (dominating) is removed and all remaining degrees are
12//! decremented; a vertex of degree 0 (isolated) is simply removed.
13//! If at any step the largest degree is neither 0 nor n-1, the graph
14//! is not threshold.
15//!
16//! This characterization applies to simple undirected graphs. For
17//! non-simple graphs or directed graphs, the function returns `false`.
18
19use crate::algorithms::properties::degree::{DegreeMode, degree_sequence};
20use crate::algorithms::properties::is_simple::is_simple;
21use crate::core::{Graph, IgraphResult};
22
23/// Check whether a graph is a threshold graph.
24///
25/// A threshold graph is built from a single vertex by repeatedly
26/// adding isolated or dominating vertices. Uses a degree-sequence
27/// peeling algorithm for O(V log V) recognition.
28///
29/// Returns `false` for directed graphs, multigraphs, or graphs with
30/// self-loops (the characterization requires simple undirected graphs).
31///
32/// An empty graph (0 vertices) is a threshold graph (vacuously).
33/// An edgeless graph is a threshold graph (all vertices are isolated).
34/// A complete graph is a threshold graph (all vertices are dominating).
35///
36/// # Examples
37///
38/// ```
39/// use rust_igraph::{Graph, is_threshold_graph};
40///
41/// // Star K_{1,3}: built as isolated, then dominating
42/// let mut g = Graph::with_vertices(4);
43/// g.add_edge(0, 1).unwrap();
44/// g.add_edge(0, 2).unwrap();
45/// g.add_edge(0, 3).unwrap();
46/// assert!(is_threshold_graph(&g).unwrap());
47///
48/// // Path P4 (0-1-2-3) is NOT threshold
49/// let mut g = Graph::with_vertices(4);
50/// g.add_edge(0, 1).unwrap();
51/// g.add_edge(1, 2).unwrap();
52/// g.add_edge(2, 3).unwrap();
53/// assert!(!is_threshold_graph(&g).unwrap());
54/// ```
55pub fn is_threshold_graph(graph: &Graph) -> IgraphResult<bool> {
56    if graph.is_directed() {
57        return Ok(false);
58    }
59
60    let n = graph.vcount() as usize;
61    if n <= 1 {
62        return Ok(true);
63    }
64
65    if !is_simple(graph)? {
66        return Ok(false);
67    }
68
69    let mut degrees = degree_sequence(graph, DegreeMode::All)?;
70    degrees.sort_unstable_by(|a, b| b.cmp(a));
71
72    let mut remaining = n;
73    let mut degs = &mut degrees[..];
74
75    while remaining > 0 {
76        let top = degs[0];
77        let last_idx = remaining.saturating_sub(1);
78
79        if top == 0 {
80            return Ok(true);
81        }
82
83        let top_usize = top as usize;
84        if top_usize == last_idx {
85            // Dominating vertex: remove it and decrement top degrees
86            degs = &mut degs[1..];
87            remaining = remaining.saturating_sub(1);
88            for d in &mut degs[..top_usize.min(remaining)] {
89                *d = d.saturating_sub(1);
90            }
91            degs.sort_unstable_by(|a, b| b.cmp(a));
92        } else if degs[last_idx] == 0 {
93            // Isolated vertex: remove it from the end
94            remaining = remaining.saturating_sub(1);
95        } else {
96            return Ok(false);
97        }
98    }
99
100    Ok(true)
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn empty_graph() {
109        let g = Graph::with_vertices(0);
110        assert!(is_threshold_graph(&g).unwrap());
111    }
112
113    #[test]
114    fn single_vertex() {
115        let g = Graph::with_vertices(1);
116        assert!(is_threshold_graph(&g).unwrap());
117    }
118
119    #[test]
120    fn edgeless_graph() {
121        let g = Graph::with_vertices(5);
122        assert!(is_threshold_graph(&g).unwrap());
123    }
124
125    #[test]
126    fn single_edge() {
127        let mut g = Graph::with_vertices(2);
128        g.add_edge(0, 1).unwrap();
129        assert!(is_threshold_graph(&g).unwrap());
130    }
131
132    #[test]
133    fn complete_k3() {
134        let mut g = Graph::with_vertices(3);
135        g.add_edge(0, 1).unwrap();
136        g.add_edge(1, 2).unwrap();
137        g.add_edge(2, 0).unwrap();
138        assert!(is_threshold_graph(&g).unwrap());
139    }
140
141    #[test]
142    fn complete_k4() {
143        let mut g = Graph::with_vertices(4);
144        for u in 0..4u32 {
145            for v in (u + 1)..4 {
146                g.add_edge(u, v).unwrap();
147            }
148        }
149        assert!(is_threshold_graph(&g).unwrap());
150    }
151
152    #[test]
153    fn star_is_threshold() {
154        let mut g = Graph::with_vertices(5);
155        for i in 1..5u32 {
156            g.add_edge(0, i).unwrap();
157        }
158        assert!(is_threshold_graph(&g).unwrap());
159    }
160
161    #[test]
162    fn path_3_is_threshold() {
163        // P3 = 0-1-2: degrees [1,2,1]. Peel: top=2, n-1=2, yes dominating.
164        // Remove, decrement → [0,0]. All zero → threshold.
165        let mut g = Graph::with_vertices(3);
166        g.add_edge(0, 1).unwrap();
167        g.add_edge(1, 2).unwrap();
168        assert!(is_threshold_graph(&g).unwrap());
169    }
170
171    #[test]
172    fn path_4_not_threshold() {
173        // P4 = 0-1-2-3: degrees [1,2,2,1].
174        // Forbidden subgraph P4 itself.
175        let mut g = Graph::with_vertices(4);
176        g.add_edge(0, 1).unwrap();
177        g.add_edge(1, 2).unwrap();
178        g.add_edge(2, 3).unwrap();
179        assert!(!is_threshold_graph(&g).unwrap());
180    }
181
182    #[test]
183    fn cycle_4_not_threshold() {
184        // C4 contains P4 and 2K2 as induced subgraphs
185        let mut g = Graph::with_vertices(4);
186        g.add_edge(0, 1).unwrap();
187        g.add_edge(1, 2).unwrap();
188        g.add_edge(2, 3).unwrap();
189        g.add_edge(3, 0).unwrap();
190        assert!(!is_threshold_graph(&g).unwrap());
191    }
192
193    #[test]
194    fn cycle_5_not_threshold() {
195        let mut g = Graph::with_vertices(5);
196        g.add_edge(0, 1).unwrap();
197        g.add_edge(1, 2).unwrap();
198        g.add_edge(2, 3).unwrap();
199        g.add_edge(3, 4).unwrap();
200        g.add_edge(4, 0).unwrap();
201        assert!(!is_threshold_graph(&g).unwrap());
202    }
203
204    #[test]
205    fn two_k2_not_threshold() {
206        // 2K2: two disjoint edges — forbidden induced subgraph
207        // degrees [1,1,1,1], top=1, n-1=3 → not dominating, bottom=1≠0
208        let mut g = Graph::with_vertices(4);
209        g.add_edge(0, 1).unwrap();
210        g.add_edge(2, 3).unwrap();
211        assert!(!is_threshold_graph(&g).unwrap());
212    }
213
214    #[test]
215    fn threshold_build_sequence() {
216        // Build: start {0}, add 1 dominating (→ K2),
217        // add 2 isolated, add 3 dominating (→ edges 3-0, 3-1, 3-2)
218        // degrees: [2,1,1,3] → sorted [3,2,1,1]
219        let mut g = Graph::with_vertices(4);
220        g.add_edge(0, 1).unwrap();
221        g.add_edge(3, 0).unwrap();
222        g.add_edge(3, 1).unwrap();
223        g.add_edge(3, 2).unwrap();
224        assert!(is_threshold_graph(&g).unwrap());
225    }
226
227    #[test]
228    fn k4_minus_edge_is_threshold() {
229        // K4 minus edge (2,3): build as {2}, add 3 isolated,
230        // add 0 dominating, add 1 dominating → threshold
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        assert!(is_threshold_graph(&g).unwrap());
238    }
239
240    #[test]
241    fn directed_returns_false() {
242        let mut g = Graph::new(3, true).unwrap();
243        g.add_edge(0, 1).unwrap();
244        g.add_edge(1, 2).unwrap();
245        assert!(!is_threshold_graph(&g).unwrap());
246    }
247
248    #[test]
249    fn self_loop_returns_false() {
250        let mut g = Graph::with_vertices(2);
251        g.add_edge(0, 0).unwrap();
252        g.add_edge(0, 1).unwrap();
253        assert!(!is_threshold_graph(&g).unwrap());
254    }
255
256    #[test]
257    fn parallel_edges_returns_false() {
258        let mut g = Graph::with_vertices(2);
259        g.add_edge(0, 1).unwrap();
260        g.add_edge(0, 1).unwrap();
261        assert!(!is_threshold_graph(&g).unwrap());
262    }
263
264    #[test]
265    fn large_star_threshold() {
266        let mut g = Graph::with_vertices(10);
267        for i in 1..10u32 {
268            g.add_edge(0, i).unwrap();
269        }
270        assert!(is_threshold_graph(&g).unwrap());
271    }
272
273    #[test]
274    fn bipartite_k23_not_threshold() {
275        // K_{2,3}: degrees [3,3,2,2,2] — not threshold
276        let mut g = Graph::with_vertices(5);
277        g.add_edge(0, 2).unwrap();
278        g.add_edge(0, 3).unwrap();
279        g.add_edge(0, 4).unwrap();
280        g.add_edge(1, 2).unwrap();
281        g.add_edge(1, 3).unwrap();
282        g.add_edge(1, 4).unwrap();
283        assert!(!is_threshold_graph(&g).unwrap());
284    }
285
286    #[test]
287    fn k_plus_isolated() {
288        // K3 + 2 isolated vertices → threshold
289        // (built: 3 isolated, then add 3,4 as dominating to existing)
290        // Actually: K3 is threshold, adding isolated vertices keeps it threshold
291        let mut g = Graph::with_vertices(5);
292        g.add_edge(0, 1).unwrap();
293        g.add_edge(1, 2).unwrap();
294        g.add_edge(2, 0).unwrap();
295        assert!(is_threshold_graph(&g).unwrap());
296    }
297}