Skip to main content

rust_igraph/algorithms/properties/
is_trivially_perfect.rs

1//! Trivially perfect graph predicate (ALGO-PR-094).
2//!
3//! A graph is trivially perfect (also called quasi-threshold) iff
4//! every connected induced subgraph has a universal vertex (a vertex
5//! adjacent to all others in the component). Equivalently, it has no
6//! induced `P_4` or `C_4`.
7//!
8//! For directed graphs, the function returns `false`.
9
10use crate::core::{Graph, IgraphResult};
11
12/// Check whether a graph is trivially perfect.
13///
14/// A trivially perfect graph (quasi-threshold graph) has no induced
15/// `P_4` or `C_4`. Equivalently, every connected induced subgraph
16/// has a universal vertex.
17///
18/// Returns `false` for directed graphs.
19///
20/// # Examples
21///
22/// ```
23/// use rust_igraph::{Graph, is_trivially_perfect};
24///
25/// // Star `K_{1,3}` is trivially perfect
26/// let mut g = Graph::with_vertices(4);
27/// g.add_edge(0, 1).unwrap();
28/// g.add_edge(0, 2).unwrap();
29/// g.add_edge(0, 3).unwrap();
30/// assert!(is_trivially_perfect(&g).unwrap());
31///
32/// // `P_4` (path of 4 vertices) is NOT trivially perfect
33/// let mut g = Graph::with_vertices(4);
34/// g.add_edge(0, 1).unwrap();
35/// g.add_edge(1, 2).unwrap();
36/// g.add_edge(2, 3).unwrap();
37/// assert!(!is_trivially_perfect(&g).unwrap());
38/// ```
39pub fn is_trivially_perfect(graph: &Graph) -> IgraphResult<bool> {
40    if graph.is_directed() {
41        return Ok(false);
42    }
43
44    let n = graph.vcount();
45    if n <= 2 {
46        return Ok(true);
47    }
48
49    // Check for induced P_4: for each edge (u, v), check if there exists
50    // w adjacent to v but not u, and x adjacent to w but not v and not u.
51    // This gives an induced path u - v - w - x.
52    //
53    // Also check for induced C_4: for each pair (u, v) at distance 2
54    // (sharing a common neighbor), check if they have two distinct
55    // common non-neighbors that form the other two vertices of a C_4.
56    //
57    // We use the universal-vertex characterization instead for clarity:
58    // For each connected component, verify there's a vertex adjacent to
59    // all others in the component, then recurse on the remaining vertices.
60
61    // Build adjacency sets for fast lookup
62    let n_usize = n as usize;
63    let mut adj = vec![vec![false; n_usize]; n_usize];
64    for v in 0..n {
65        let nbrs = graph.neighbors(v)?;
66        for &w in &nbrs {
67            adj[v as usize][w as usize] = true;
68        }
69    }
70
71    // Find connected components via BFS on our adjacency matrix
72    let mut visited = vec![false; n_usize];
73    let mut component = Vec::new();
74
75    for start in 0..n {
76        if visited[start as usize] {
77            continue;
78        }
79
80        // BFS to find this component
81        component.clear();
82        let mut queue = std::collections::VecDeque::new();
83        queue.push_back(start);
84        visited[start as usize] = true;
85
86        while let Some(v) = queue.pop_front() {
87            component.push(v);
88            for w in 0..n {
89                if !visited[w as usize] && adj[v as usize][w as usize] {
90                    visited[w as usize] = true;
91                    queue.push_back(w);
92                }
93            }
94        }
95
96        // Check this component recursively
97        if !check_component_trivially_perfect(&adj, &component) {
98            return Ok(false);
99        }
100    }
101
102    Ok(true)
103}
104
105fn check_component_trivially_perfect(adj: &[Vec<bool>], component: &[u32]) -> bool {
106    let size = component.len();
107    if size <= 2 {
108        return true;
109    }
110
111    // Find a universal vertex in this component
112    let mut universal = None;
113    for &v in component {
114        let deg_in_comp = component
115            .iter()
116            .filter(|&&w| w != v && adj[v as usize][w as usize])
117            .count();
118        if deg_in_comp == size - 1 {
119            universal = Some(v);
120            break;
121        }
122    }
123
124    let Some(univ) = universal else {
125        return false;
126    };
127
128    // Remove the universal vertex and recurse on each resulting component
129    let remaining: Vec<u32> = component.iter().copied().filter(|&v| v != univ).collect();
130
131    // Find connected components among remaining vertices
132    let mut rem_visited = vec![false; remaining.len()];
133    let mut sub_component = Vec::new();
134
135    for i in 0..remaining.len() {
136        if rem_visited[i] {
137            continue;
138        }
139
140        sub_component.clear();
141        let mut queue = std::collections::VecDeque::new();
142        queue.push_back(i);
143        rem_visited[i] = true;
144
145        while let Some(idx) = queue.pop_front() {
146            sub_component.push(remaining[idx]);
147            for j in 0..remaining.len() {
148                if !rem_visited[j] && adj[remaining[idx] as usize][remaining[j] as usize] {
149                    rem_visited[j] = true;
150                    queue.push_back(j);
151                }
152            }
153        }
154
155        if !check_component_trivially_perfect(adj, &sub_component) {
156            return false;
157        }
158    }
159
160    true
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn empty_graph() {
169        let g = Graph::with_vertices(0);
170        assert!(is_trivially_perfect(&g).unwrap());
171    }
172
173    #[test]
174    fn single_vertex() {
175        let g = Graph::with_vertices(1);
176        assert!(is_trivially_perfect(&g).unwrap());
177    }
178
179    #[test]
180    fn single_edge() {
181        let mut g = Graph::with_vertices(2);
182        g.add_edge(0, 1).unwrap();
183        assert!(is_trivially_perfect(&g).unwrap());
184    }
185
186    #[test]
187    fn triangle() {
188        let mut g = Graph::with_vertices(3);
189        g.add_edge(0, 1).unwrap();
190        g.add_edge(1, 2).unwrap();
191        g.add_edge(2, 0).unwrap();
192        assert!(is_trivially_perfect(&g).unwrap());
193    }
194
195    #[test]
196    fn star_k14() {
197        let mut g = Graph::with_vertices(5);
198        g.add_edge(0, 1).unwrap();
199        g.add_edge(0, 2).unwrap();
200        g.add_edge(0, 3).unwrap();
201        g.add_edge(0, 4).unwrap();
202        assert!(is_trivially_perfect(&g).unwrap());
203    }
204
205    #[test]
206    fn complete_k4() {
207        let mut g = Graph::with_vertices(4);
208        g.add_edge(0, 1).unwrap();
209        g.add_edge(0, 2).unwrap();
210        g.add_edge(0, 3).unwrap();
211        g.add_edge(1, 2).unwrap();
212        g.add_edge(1, 3).unwrap();
213        g.add_edge(2, 3).unwrap();
214        assert!(is_trivially_perfect(&g).unwrap());
215    }
216
217    #[test]
218    fn p4_not_trivially_perfect() {
219        // P_4: 0-1-2-3 — has an induced P_4
220        let mut g = Graph::with_vertices(4);
221        g.add_edge(0, 1).unwrap();
222        g.add_edge(1, 2).unwrap();
223        g.add_edge(2, 3).unwrap();
224        assert!(!is_trivially_perfect(&g).unwrap());
225    }
226
227    #[test]
228    fn c4_not_trivially_perfect() {
229        // C_4: 0-1-2-3-0 — has an induced C_4
230        let mut g = Graph::with_vertices(4);
231        g.add_edge(0, 1).unwrap();
232        g.add_edge(1, 2).unwrap();
233        g.add_edge(2, 3).unwrap();
234        g.add_edge(3, 0).unwrap();
235        assert!(!is_trivially_perfect(&g).unwrap());
236    }
237
238    #[test]
239    fn c5_not_trivially_perfect() {
240        let mut g = Graph::with_vertices(5);
241        g.add_edge(0, 1).unwrap();
242        g.add_edge(1, 2).unwrap();
243        g.add_edge(2, 3).unwrap();
244        g.add_edge(3, 4).unwrap();
245        g.add_edge(4, 0).unwrap();
246        assert!(!is_trivially_perfect(&g).unwrap());
247    }
248
249    #[test]
250    fn disconnected_trivially_perfect() {
251        // Two isolated edges: trivially perfect
252        let mut g = Graph::with_vertices(4);
253        g.add_edge(0, 1).unwrap();
254        g.add_edge(2, 3).unwrap();
255        assert!(is_trivially_perfect(&g).unwrap());
256    }
257
258    #[test]
259    fn disconnected_with_p4() {
260        // Triangle + P_4
261        let mut g = Graph::with_vertices(7);
262        g.add_edge(0, 1).unwrap();
263        g.add_edge(1, 2).unwrap();
264        g.add_edge(2, 0).unwrap();
265        g.add_edge(3, 4).unwrap();
266        g.add_edge(4, 5).unwrap();
267        g.add_edge(5, 6).unwrap();
268        assert!(!is_trivially_perfect(&g).unwrap());
269    }
270
271    #[test]
272    fn fan_with_p4_not_trivially_perfect() {
273        // Fan: universal vertex 0 connected to path 1-2-3-4
274        // Removing 0 leaves P_4 → not trivially perfect
275        let mut g = Graph::with_vertices(5);
276        g.add_edge(0, 1).unwrap();
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(2, 3).unwrap();
282        g.add_edge(3, 4).unwrap();
283        assert!(!is_trivially_perfect(&g).unwrap());
284    }
285
286    #[test]
287    fn fan_with_p3_trivially_perfect() {
288        // Universal vertex 0 connected to path 1-2-3
289        // Removing 0 leaves P_3; vertex 2 is universal in P_3.
290        // Removing 2 leaves {1, 3} disconnected, each trivially perfect.
291        let mut g = Graph::with_vertices(4);
292        g.add_edge(0, 1).unwrap();
293        g.add_edge(0, 2).unwrap();
294        g.add_edge(0, 3).unwrap();
295        g.add_edge(1, 2).unwrap();
296        g.add_edge(2, 3).unwrap();
297        assert!(is_trivially_perfect(&g).unwrap());
298    }
299
300    #[test]
301    fn nested_stars() {
302        // 0 connected to all; 1 connected to {2, 3}
303        // This is a "threshold graph" = trivially perfect
304        let mut g = Graph::with_vertices(4);
305        g.add_edge(0, 1).unwrap();
306        g.add_edge(0, 2).unwrap();
307        g.add_edge(0, 3).unwrap();
308        g.add_edge(1, 2).unwrap();
309        g.add_edge(1, 3).unwrap();
310        assert!(is_trivially_perfect(&g).unwrap());
311    }
312
313    #[test]
314    fn directed_returns_false() {
315        let mut g = Graph::new(3, true).unwrap();
316        g.add_edge(0, 1).unwrap();
317        g.add_edge(1, 2).unwrap();
318        g.add_edge(2, 0).unwrap();
319        assert!(!is_trivially_perfect(&g).unwrap());
320    }
321
322    #[test]
323    fn bull_graph_not_trivially_perfect() {
324        // Bull: triangle 0-1-2 with pendant edges 1-3 and 2-4
325        // Induced path: 3-1-2-4 → P_4
326        let mut g = Graph::with_vertices(5);
327        g.add_edge(0, 1).unwrap();
328        g.add_edge(0, 2).unwrap();
329        g.add_edge(1, 2).unwrap();
330        g.add_edge(1, 3).unwrap();
331        g.add_edge(2, 4).unwrap();
332        assert!(!is_trivially_perfect(&g).unwrap());
333    }
334
335    #[test]
336    fn isolated_vertices() {
337        let g = Graph::with_vertices(5);
338        assert!(is_trivially_perfect(&g).unwrap());
339    }
340}