Skip to main content

rust_igraph/algorithms/properties/
perfect.rs

1//! Perfect-graph test (ALGO-PR-018).
2//!
3//! Counterpart of `igraph_is_perfect()` from
4//! `references/igraph/src/properties/perfect.c:55-190`.
5//!
6//! A graph is *perfect* when, for every induced subgraph, the chromatic
7//! number equals the size of the largest clique. By the Strong Perfect
8//! Graph Theorem (Chudnovsky–Robertson–Seymour–Thomas, conjectured by
9//! Berge) a graph is perfect iff neither it nor its complement contains an
10//! induced odd cycle of length ≥ 5 (an "odd hole").
11//!
12//! The implementation is a faithful port of upstream's decision cascade,
13//! composed entirely from primitives already ported in this crate:
14//!
15//! 1. Trivial perfect cases: fewer than 5 vertices; very sparse / very
16//!    dense small graphs (≤ 4 edges, or within 5 edges of complete).
17//! 2. Bipartite or chordal graphs are perfect (so are their complements —
18//!    the Weak Perfect Graph Theorem lets us also test the complement).
19//! 3. An odd girth > 3 in the graph or its complement proves *not* perfect.
20//! 4. Otherwise search for an induced odd hole of length ≥ 5 in both the
21//!    graph and its complement via induced LAD subgraph isomorphism.
22//!
23//! Scope mirrors upstream: undirected, simple graphs only (directed or
24//! non-simple input is an error).
25
26use crate::algorithms::chordality::is_chordal;
27use crate::algorithms::constructors::ring::ring_graph;
28use crate::algorithms::isomorphism::lad::subisomorphic_lad;
29use crate::algorithms::operators::complementer::complementer;
30use crate::algorithms::properties::girth::girth;
31use crate::algorithms::properties::is_bipartite::is_bipartite;
32use crate::algorithms::properties::is_simple::is_simple;
33use crate::core::{Graph, IgraphError, IgraphResult};
34
35/// Returns `true` when `graph` is a perfect graph.
36///
37/// The graph must be **undirected** and **simple**; otherwise an
38/// [`IgraphError::InvalidArgument`] is returned, matching upstream's
39/// `IGRAPH_EINVAL`.
40///
41/// Time complexity: worst case exponential (the odd-hole search runs
42/// induced subgraph isomorphism), but the bipartite / chordal / girth
43/// fast paths resolve most inputs cheaply.
44///
45/// # Examples
46///
47/// ```
48/// use rust_igraph::{Graph, is_perfect};
49///
50/// // The 5-cycle C5 is the smallest imperfect graph (it is its own odd hole).
51/// let mut c5 = Graph::new(5, false)?;
52/// for (u, v) in [(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)] {
53///     c5.add_edge(u, v)?;
54/// }
55/// assert!(!is_perfect(&c5)?);
56///
57/// // A tree (here a path) is bipartite, hence perfect.
58/// let mut path = Graph::new(4, false)?;
59/// for (u, v) in [(0, 1), (1, 2), (2, 3)] {
60///     path.add_edge(u, v)?;
61/// }
62/// assert!(is_perfect(&path)?);
63/// # Ok::<(), rust_igraph::IgraphError>(())
64/// ```
65pub fn is_perfect(graph: &Graph) -> IgraphResult<bool> {
66    if graph.is_directed() {
67        return Err(IgraphError::InvalidArgument(
68            "The concept of perfect graphs is only defined for undirected graphs.".to_string(),
69        ));
70    }
71
72    if !is_simple(graph)? {
73        return Err(IgraphError::InvalidArgument(
74            "Perfect graph testing is implemented for simple graphs only. Simplify the graph."
75                .to_string(),
76        ));
77    }
78
79    let no_of_nodes = graph.vcount();
80    let no_of_edges = graph.ecount();
81
82    // All graphs with fewer than 5 vertices are perfect.
83    if no_of_nodes < 5 {
84        return Ok(true);
85    }
86
87    // Graphs with fewer than 5 edges, or whose complement has fewer than 5
88    // edges, are perfect. Bounded to small graphs to avoid overflow and
89    // because the check's usefulness vanishes as the graph grows.
90    let n = u64::from(no_of_nodes);
91    let max_edges = (n - 1) * n / 2;
92    if no_of_nodes < 10_000
93        && (no_of_edges < 5 || (max_edges >= 5 && no_of_edges as u64 > max_edges - 5))
94    {
95        return Ok(true);
96    }
97
98    // Bipartite and chordal graphs are perfect.
99    if is_bipartite(graph)?.is_bipartite {
100        return Ok(true);
101    }
102    if is_chordal(graph, None)?.chordal {
103        return Ok(true);
104    }
105
106    // Weak Perfect Graph Theorem: G is perfect iff its complement is.
107    let comp = complementer(graph, false)?;
108
109    if is_bipartite(&comp)?.is_bipartite {
110        return Ok(true);
111    }
112    if is_chordal(&comp, None)?.chordal {
113        return Ok(true);
114    }
115
116    // Past the bipartite check both girths are finite (bipartite also
117    // catches forests). A finite odd girth > 3 is an odd hole on its own.
118    let Some(girth_g) = girth(graph)? else {
119        return Ok(true);
120    };
121    if girth_g > 3 && girth_g % 2 == 1 {
122        return Ok(false);
123    }
124
125    let Some(girth_c) = girth(&comp)? else {
126        return Ok(true);
127    };
128    if girth_c > 3 && girth_c % 2 == 1 {
129        return Ok(false);
130    }
131
132    // Strong Perfect Graph Theorem: search for an induced odd hole of
133    // length ≥ 5 in either the graph or its complement.
134    let min_girth = girth_g.min(girth_c);
135    let mut cycle_len = if min_girth % 2 == 0 {
136        min_girth + 1
137    } else {
138        min_girth + 2
139    };
140
141    while cycle_len <= no_of_nodes {
142        let cycle = ring_graph(cycle_len, false, false, true)?;
143
144        if cycle_len > girth_g && subisomorphic_lad(&cycle, graph, None, true)?.iso {
145            return Ok(false);
146        }
147        if cycle_len > girth_c && subisomorphic_lad(&cycle, &comp, None, true)?.iso {
148            return Ok(false);
149        }
150
151        cycle_len += 2;
152    }
153
154    Ok(true)
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    fn undirected(n: u32, edges: &[(u32, u32)]) -> Graph {
162        let mut g = Graph::new(n, false).expect("graph init");
163        for &(a, b) in edges {
164            g.add_edge(a, b).expect("add edge");
165        }
166        g
167    }
168
169    fn ring(n: u32) -> Graph {
170        ring_graph(n, false, false, true).expect("ring")
171    }
172
173    #[test]
174    fn null_graph_is_perfect() {
175        let g = Graph::new(0, false).expect("graph");
176        assert!(is_perfect(&g).expect("is_perfect"));
177    }
178
179    #[test]
180    fn singleton_is_perfect() {
181        let g = Graph::new(1, false).expect("graph");
182        assert!(is_perfect(&g).expect("is_perfect"));
183    }
184
185    #[test]
186    fn empty_two_vertices_is_perfect() {
187        let g = Graph::new(2, false).expect("graph");
188        assert!(is_perfect(&g).expect("is_perfect"));
189    }
190
191    #[test]
192    fn small_graphs_under_five_vertices_are_perfect() {
193        // K4 — under the 5-vertex trivial bound.
194        let g = undirected(4, &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]);
195        assert!(is_perfect(&g).expect("is_perfect"));
196    }
197
198    #[test]
199    fn c5_is_not_perfect() {
200        assert!(!is_perfect(&ring(5)).expect("is_perfect"));
201    }
202
203    #[test]
204    fn c7_is_not_perfect() {
205        assert!(!is_perfect(&ring(7)).expect("is_perfect"));
206    }
207
208    #[test]
209    fn c6_is_perfect() {
210        // Even cycles are bipartite, hence perfect.
211        assert!(is_perfect(&ring(6)).expect("is_perfect"));
212    }
213
214    #[test]
215    fn complement_of_c7_is_not_perfect() {
216        let c7 = ring(7);
217        let comp = complementer(&c7, false).expect("complement");
218        assert!(!is_perfect(&comp).expect("is_perfect"));
219    }
220
221    #[test]
222    fn paley_order_9_is_perfect() {
223        // Paley graph of order 9 — self-complementary, perfect.
224        let g = undirected(
225            9,
226            &[
227                (0, 1),
228                (0, 3),
229                (0, 6),
230                (0, 2),
231                (1, 2),
232                (1, 4),
233                (1, 7),
234                (2, 5),
235                (2, 8),
236                (3, 4),
237                (3, 5),
238                (3, 6),
239                (4, 5),
240                (4, 7),
241                (5, 8),
242                (6, 7),
243                (7, 8),
244                (6, 8),
245            ],
246        );
247        assert!(is_perfect(&g).expect("is_perfect"));
248    }
249
250    #[test]
251    fn house_graph_is_perfect() {
252        // "House": a C5 would be imperfect, but the House (square + roof
253        // triangle) is chordal-free yet perfect.
254        let g = undirected(5, &[(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]);
255        assert!(is_perfect(&g).expect("is_perfect"));
256    }
257
258    #[test]
259    fn directed_graph_errors() {
260        let mut g = Graph::new(5, true).expect("graph");
261        g.add_edge(0, 1).expect("edge");
262        assert!(is_perfect(&g).is_err());
263    }
264
265    #[test]
266    fn non_simple_graph_errors() {
267        let mut g = Graph::new(5, false).expect("graph");
268        g.add_edge(0, 1).expect("edge");
269        g.add_edge(0, 1).expect("parallel edge");
270        assert!(is_perfect(&g).is_err());
271    }
272}