Skip to main content

rust_igraph/algorithms/properties/
is_chordal_bipartite.rs

1//! Chordal bipartite graph predicate (ALGO-PR-111).
2//!
3//! A bipartite graph is chordal bipartite if every cycle of length ≥ 6
4//! has a chord. Equivalently, a bipartite graph with no induced `C_6`
5//! or longer. This is NOT the same as being both chordal and bipartite
6//! (which would force a forest).
7//!
8//! Non-bipartite and directed graphs return `false`.
9
10use crate::algorithms::connectivity::{ConnectednessMode, is_connected};
11use crate::algorithms::properties::is_bipartite::is_bipartite;
12use crate::core::{Graph, IgraphResult};
13
14/// Check whether a graph is chordal bipartite.
15///
16/// A bipartite graph is chordal bipartite if it has no induced cycle
17/// of length ≥ 6. Returns `false` for non-bipartite or directed graphs.
18///
19/// Uses a perfect elimination ordering (PEO) approach on the bipartite
20/// adjacency: repeatedly finds a vertex whose neighborhood forms a
21/// "bisimplicial" structure and removes it.
22///
23/// # Examples
24///
25/// ```
26/// use rust_igraph::{Graph, is_chordal_bipartite};
27///
28/// // Complete bipartite `K_{2,3}` is chordal bipartite
29/// let mut g = Graph::with_vertices(5);
30/// g.add_edge(0, 2).unwrap();
31/// g.add_edge(0, 3).unwrap();
32/// g.add_edge(0, 4).unwrap();
33/// g.add_edge(1, 2).unwrap();
34/// g.add_edge(1, 3).unwrap();
35/// g.add_edge(1, 4).unwrap();
36/// assert!(is_chordal_bipartite(&g).unwrap());
37///
38/// // `C_6` is bipartite but NOT chordal bipartite
39/// let mut g = Graph::with_vertices(6);
40/// g.add_edge(0, 1).unwrap();
41/// g.add_edge(1, 2).unwrap();
42/// g.add_edge(2, 3).unwrap();
43/// g.add_edge(3, 4).unwrap();
44/// g.add_edge(4, 5).unwrap();
45/// g.add_edge(5, 0).unwrap();
46/// assert!(!is_chordal_bipartite(&g).unwrap());
47/// ```
48pub fn is_chordal_bipartite(graph: &Graph) -> IgraphResult<bool> {
49    if graph.is_directed() {
50        return Ok(false);
51    }
52
53    let n = graph.vcount();
54    if n <= 4 {
55        // Any bipartite graph on ≤ 4 vertices is chordal bipartite
56        // (need ≥ 6 vertices for C_6).
57        return is_bipartite(graph).map(|r| r.is_bipartite);
58    }
59
60    let bip = is_bipartite(graph)?;
61    if !bip.is_bipartite {
62        return Ok(false);
63    }
64
65    // For disconnected graphs, each component must be chordal bipartite.
66    if !is_connected(graph, ConnectednessMode::Weak)? {
67        return check_components_chordal_bipartite(graph);
68    }
69
70    check_single_component(graph, n)
71}
72
73/// Check each connected component independently.
74fn check_components_chordal_bipartite(graph: &Graph) -> IgraphResult<bool> {
75    let n = graph.vcount();
76    let n_usize = n as usize;
77    let mut visited = vec![false; n_usize];
78    let mut nbrs_cache: Vec<Vec<u32>> = Vec::with_capacity(n_usize);
79    for v in 0..n {
80        nbrs_cache.push(graph.neighbors(v)?);
81    }
82
83    for start in 0..n_usize {
84        if visited[start] {
85            continue;
86        }
87        // BFS to find component
88        let mut component = Vec::new();
89        let mut queue = std::collections::VecDeque::new();
90        visited[start] = true;
91        queue.push_back(start);
92        while let Some(u) = queue.pop_front() {
93            component.push(u);
94            for &w in &nbrs_cache[u] {
95                let wi = w as usize;
96                if !visited[wi] {
97                    visited[wi] = true;
98                    queue.push_back(wi);
99                }
100            }
101        }
102
103        if component.len() >= 6 && !check_component_chordal_bipartite(&nbrs_cache, &component) {
104            return Ok(false);
105        }
106    }
107
108    Ok(true)
109}
110
111/// Check a single connected component for chordal bipartiteness.
112fn check_component_chordal_bipartite(nbrs_cache: &[Vec<u32>], component: &[usize]) -> bool {
113    // Build local adjacency for the component using a vertex map.
114    let max_id = component.iter().copied().max().unwrap_or(0);
115    let mut global_to_local = vec![usize::MAX; max_id + 1];
116    for (i, &v) in component.iter().enumerate() {
117        global_to_local[v] = i;
118    }
119
120    let cn = component.len();
121    let mut adj = vec![vec![false; cn]; cn];
122    for &v in component {
123        let li = global_to_local[v];
124        for &w in &nbrs_cache[v] {
125            let wi = w as usize;
126            if wi <= max_id && global_to_local[wi] != usize::MAX {
127                adj[li][global_to_local[wi]] = true;
128            }
129        }
130    }
131
132    is_chordal_bipartite_from_adj(&adj, cn)
133}
134
135/// Check a single connected graph using adjacency matrix.
136fn check_single_component(graph: &Graph, n: u32) -> IgraphResult<bool> {
137    let n_usize = n as usize;
138    let mut adj = vec![vec![false; n_usize]; n_usize];
139    for v in 0..n {
140        let nbrs = graph.neighbors(v)?;
141        for &w in &nbrs {
142            adj[v as usize][w as usize] = true;
143        }
144    }
145    Ok(is_chordal_bipartite_from_adj(&adj, n_usize))
146}
147
148/// Core algorithm: check chordal bipartiteness via bisimplicial
149/// elimination. A bipartite graph is chordal bipartite iff it has a
150/// perfect elimination ordering where each eliminated vertex is
151/// bisimplicial (its neighbors form a complete bipartite subgraph
152/// with their common neighborhood).
153///
154/// Simpler approach: check that no induced `C_6` exists. Since
155/// checking all `C_{2k}` for k≥3 is expensive, we use the
156/// bisimplicial elimination characterization.
157fn is_chordal_bipartite_from_adj(adj: &[Vec<bool>], n: usize) -> bool {
158    // A vertex v is bisimplicial if: for every pair of neighbors
159    // u, w of v, every common neighbor of u and w (other than v)
160    // that is on the same side as v is also a neighbor of v.
161    // Equivalently: N(u) ∩ N(w) ⊆ N(v) ∪ {v} for all u,w ∈ N(v)
162    // on one side, and the neighborhoods of all N(v) restricted to
163    // v's side are nested (form a chain under inclusion).
164
165    // Practical approach: iteratively remove bisimplicial vertices.
166    // If we can remove all vertices, it's chordal bipartite.
167
168    let mut removed = vec![false; n];
169    let mut remaining = n;
170
171    while remaining > 0 {
172        let mut found = false;
173        for v in 0..n {
174            if removed[v] {
175                continue;
176            }
177
178            if is_bisimplicial(adj, v, &removed, n) {
179                removed[v] = true;
180                remaining -= 1;
181                found = true;
182                break;
183            }
184        }
185
186        if !found {
187            return false;
188        }
189    }
190
191    true
192}
193
194/// Check if vertex v is bisimplicial in the remaining graph.
195/// In a bipartite graph, v is bisimplicial if the subgraph induced by
196/// N(v) and (N(N(v)) \ {v}) is complete bipartite: every second-neighbor
197/// of v must be adjacent to ALL vertices in N(v).
198fn is_bisimplicial(adj: &[Vec<bool>], v: usize, removed: &[bool], n: usize) -> bool {
199    let nbrs_v: Vec<usize> = (0..n).filter(|&u| !removed[u] && adj[v][u]).collect();
200
201    if nbrs_v.is_empty() {
202        return true;
203    }
204
205    // For each neighbor u of v, every active neighbor x of u (x ≠ v)
206    // must be adjacent to ALL of N(v).
207    for &u in &nbrs_v {
208        for x in 0..n {
209            if removed[x] || x == v || !adj[u][x] {
210                continue;
211            }
212            // x is a second-neighbor of v via u — must connect to all of N(v)
213            for &w in &nbrs_v {
214                if !adj[x][w] {
215                    return false;
216                }
217            }
218        }
219    }
220
221    true
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn empty_graph() {
230        let g = Graph::with_vertices(0);
231        assert!(is_chordal_bipartite(&g).unwrap());
232    }
233
234    #[test]
235    fn single_vertex() {
236        let g = Graph::with_vertices(1);
237        assert!(is_chordal_bipartite(&g).unwrap());
238    }
239
240    #[test]
241    fn single_edge() {
242        let mut g = Graph::with_vertices(2);
243        g.add_edge(0, 1).unwrap();
244        assert!(is_chordal_bipartite(&g).unwrap());
245    }
246
247    #[test]
248    fn c4_chordal_bipartite() {
249        // `C_4` is bipartite and chordal bipartite (no `C_6`)
250        let mut g = Graph::with_vertices(4);
251        g.add_edge(0, 1).unwrap();
252        g.add_edge(1, 2).unwrap();
253        g.add_edge(2, 3).unwrap();
254        g.add_edge(3, 0).unwrap();
255        assert!(is_chordal_bipartite(&g).unwrap());
256    }
257
258    #[test]
259    fn c6_not_chordal_bipartite() {
260        let mut g = Graph::with_vertices(6);
261        g.add_edge(0, 1).unwrap();
262        g.add_edge(1, 2).unwrap();
263        g.add_edge(2, 3).unwrap();
264        g.add_edge(3, 4).unwrap();
265        g.add_edge(4, 5).unwrap();
266        g.add_edge(5, 0).unwrap();
267        assert!(!is_chordal_bipartite(&g).unwrap());
268    }
269
270    #[test]
271    fn complete_bipartite_k23() {
272        let mut g = Graph::with_vertices(5);
273        g.add_edge(0, 2).unwrap();
274        g.add_edge(0, 3).unwrap();
275        g.add_edge(0, 4).unwrap();
276        g.add_edge(1, 2).unwrap();
277        g.add_edge(1, 3).unwrap();
278        g.add_edge(1, 4).unwrap();
279        assert!(is_chordal_bipartite(&g).unwrap());
280    }
281
282    #[test]
283    fn complete_bipartite_k33() {
284        let mut g = Graph::with_vertices(6);
285        for i in 0..3u32 {
286            for j in 3..6u32 {
287                g.add_edge(i, j).unwrap();
288            }
289        }
290        assert!(is_chordal_bipartite(&g).unwrap());
291    }
292
293    #[test]
294    fn tree_chordal_bipartite() {
295        // Trees are bipartite and chordal bipartite (no cycles at all)
296        let mut g = Graph::with_vertices(5);
297        g.add_edge(0, 1).unwrap();
298        g.add_edge(1, 2).unwrap();
299        g.add_edge(1, 3).unwrap();
300        g.add_edge(3, 4).unwrap();
301        assert!(is_chordal_bipartite(&g).unwrap());
302    }
303
304    #[test]
305    fn triangle_not_bipartite() {
306        // Triangle is not bipartite → not chordal bipartite
307        let mut g = Graph::with_vertices(3);
308        g.add_edge(0, 1).unwrap();
309        g.add_edge(1, 2).unwrap();
310        g.add_edge(2, 0).unwrap();
311        assert!(!is_chordal_bipartite(&g).unwrap());
312    }
313
314    #[test]
315    fn directed_returns_false() {
316        let mut g = Graph::new(3, true).unwrap();
317        g.add_edge(0, 1).unwrap();
318        g.add_edge(1, 2).unwrap();
319        assert!(!is_chordal_bipartite(&g).unwrap());
320    }
321
322    #[test]
323    fn path_chordal_bipartite() {
324        let mut g = Graph::with_vertices(6);
325        g.add_edge(0, 1).unwrap();
326        g.add_edge(1, 2).unwrap();
327        g.add_edge(2, 3).unwrap();
328        g.add_edge(3, 4).unwrap();
329        g.add_edge(4, 5).unwrap();
330        assert!(is_chordal_bipartite(&g).unwrap());
331    }
332
333    #[test]
334    fn star_chordal_bipartite() {
335        let mut g = Graph::with_vertices(5);
336        g.add_edge(0, 1).unwrap();
337        g.add_edge(0, 2).unwrap();
338        g.add_edge(0, 3).unwrap();
339        g.add_edge(0, 4).unwrap();
340        assert!(is_chordal_bipartite(&g).unwrap());
341    }
342
343    #[test]
344    fn c8_not_chordal_bipartite() {
345        let mut g = Graph::with_vertices(8);
346        for i in 0..8u32 {
347            g.add_edge(i, (i + 1) % 8).unwrap();
348        }
349        assert!(!is_chordal_bipartite(&g).unwrap());
350    }
351
352    #[test]
353    fn disconnected_bipartite() {
354        // Two `C_4` components → chordal bipartite
355        let mut g = Graph::with_vertices(8);
356        g.add_edge(0, 1).unwrap();
357        g.add_edge(1, 2).unwrap();
358        g.add_edge(2, 3).unwrap();
359        g.add_edge(3, 0).unwrap();
360        g.add_edge(4, 5).unwrap();
361        g.add_edge(5, 6).unwrap();
362        g.add_edge(6, 7).unwrap();
363        g.add_edge(7, 4).unwrap();
364        assert!(is_chordal_bipartite(&g).unwrap());
365    }
366}