Skip to main content

rust_igraph/algorithms/properties/
is_weakly_chordal.rs

1//! Weakly chordal graph predicate (ALGO-PR-121).
2//!
3//! A graph is weakly chordal (also called weakly triangulated) if
4//! neither G nor its complement contains an induced cycle of length
5//! ≥ 5. Equivalently, every induced cycle in both G and its
6//! complement has length at most 4.
7//!
8//! Every chordal graph is weakly chordal (no induced `C_k` for k ≥ 4).
9//! Every co-chordal graph (complement is chordal) is weakly chordal.
10//!
11//! Directed graphs are treated as undirected.
12
13use crate::core::{Graph, IgraphResult};
14
15/// Check whether a graph is weakly chordal.
16///
17/// A graph is weakly chordal if neither G nor its complement has an
18/// induced cycle of length ≥ 5. Uses chordality checks on both G
19/// and its complement; if both are chordal, G is weakly chordal.
20/// Otherwise, checks for induced ``C_k`` (k ≥ 5) in the non-chordal
21/// graph(s).
22///
23/// Directed graphs are treated as undirected.
24///
25/// # Examples
26///
27/// ```
28/// use rust_igraph::{Graph, is_weakly_chordal};
29///
30/// // `K_4`: chordal → weakly chordal
31/// let mut g = Graph::with_vertices(4);
32/// for i in 0..4u32 {
33///     for j in (i+1)..4 {
34///         g.add_edge(i, j).unwrap();
35///     }
36/// }
37/// assert!(is_weakly_chordal(&g).unwrap());
38///
39/// // `C_5` is NOT weakly chordal (is its own induced C_5)
40/// let mut g = Graph::with_vertices(5);
41/// g.add_edge(0, 1).unwrap();
42/// g.add_edge(1, 2).unwrap();
43/// g.add_edge(2, 3).unwrap();
44/// g.add_edge(3, 4).unwrap();
45/// g.add_edge(4, 0).unwrap();
46/// assert!(!is_weakly_chordal(&g).unwrap());
47/// ```
48pub fn is_weakly_chordal(graph: &Graph) -> IgraphResult<bool> {
49    let n = graph.vcount();
50    if n <= 4 {
51        return Ok(true);
52    }
53
54    let n_usize = n as usize;
55    let mut adj = vec![vec![false; n_usize]; n_usize];
56    for v in 0..n {
57        let nbrs = graph.neighbors(v)?;
58        for &w in &nbrs {
59            adj[v as usize][w as usize] = true;
60            adj[w as usize][v as usize] = true;
61        }
62    }
63
64    // Check if G has an induced `C_k` for k ≥ 5
65    if has_long_induced_cycle(&adj, n_usize) {
66        return Ok(false);
67    }
68
69    // Build complement adjacency
70    let mut comp = vec![vec![false; n_usize]; n_usize];
71    for i in 0..n_usize {
72        for j in (i + 1)..n_usize {
73            if !adj[i][j] {
74                comp[i][j] = true;
75                comp[j][i] = true;
76            }
77        }
78    }
79
80    // Check if complement has an induced `C_k` for k ≥ 5
81    if has_long_induced_cycle(&comp, n_usize) {
82        return Ok(false);
83    }
84
85    Ok(true)
86}
87
88/// Check if a graph (given by adjacency matrix) has an induced cycle
89/// of length ≥ 5. Uses DFS to find chordless cycles.
90fn has_long_induced_cycle(adj: &[Vec<bool>], n: usize) -> bool {
91    // A graph has no induced `C_k` (k ≥ 5) iff it is "hole-free for k≥5".
92    // We can check this by looking for chordless cycles of length ≥ 5.
93    // Approach: for each edge (u,v), try to find a chordless path from
94    // v back to u of length ≥ 4 (so total cycle ≥ 5) that avoids
95    // non-path neighbors of u.
96
97    // Simpler approach: check if chordal. If chordal, no induced `C_k`
98    // for k ≥ 4, so certainly no k ≥ 5.
99    // If not chordal, there exists an induced `C_k` for k ≥ 4.
100    // If k = 4, that's fine (weakly chordal allows C_4).
101    // If k ≥ 5, that's a violation.
102
103    // So we need to check: is there an induced `C_k` for k ≥ 5?
104    // If the graph is chordal, return false.
105    // If the graph has an induced C_4 but no induced `C_k` (k ≥ 5),
106    // return false.
107
108    // For the DFS approach: enumerate chordless cycles.
109    // For each vertex u, for each pair of non-adjacent neighbors (v,w),
110    // try to find a chordless path from v to w not through u and not
111    // using any other neighbor of u (to ensure chordless cycle with u).
112    // If path length ≥ 3, cycle length = path + 2 ≥ 5.
113
114    for u in 0..n {
115        let nbrs_u: Vec<usize> = (0..n).filter(|&v| adj[u][v]).collect();
116
117        for (i, &v) in nbrs_u.iter().enumerate() {
118            for &w in &nbrs_u[(i + 1)..] {
119                if adj[v][w] {
120                    continue;
121                }
122                // v and w are non-adjacent neighbors of u.
123                // Find chordless path from v to w not through u,
124                // where internal vertices are not neighbors of u
125                // (to ensure the cycle u-v-...-w-u is chordless).
126                if chordless_path_exists(adj, v, w, u, &nbrs_u, n, 3) {
127                    return true;
128                }
129            }
130        }
131    }
132
133    false
134}
135
136/// Check if there exists a chordless path from `start` to `end` of
137/// length ≥ `min_len`, not passing through `excluded`, where
138/// internal vertices are not in `forbidden` (neighbors of the cycle
139/// anchor) and are not adjacent to both `start`'s predecessor and
140/// `end` (to maintain chordlessness).
141fn chordless_path_exists(
142    adj: &[Vec<bool>],
143    start: usize,
144    end: usize,
145    excluded: usize,
146    forbidden_set: &[usize],
147    n: usize,
148    min_len: usize,
149) -> bool {
150    // BFS/DFS to find path from start to end of length ≥ min_len
151    // through vertices that are not in forbidden_set and not excluded.
152    let mut forbidden = vec![false; n];
153    forbidden[excluded] = true;
154    for &f in forbidden_set {
155        if f != start && f != end {
156            forbidden[f] = true;
157        }
158    }
159
160    // DFS with path tracking
161    let mut path = vec![start];
162    let mut visited = vec![false; n];
163    visited[start] = true;
164    visited[excluded] = true;
165
166    dfs_chordless(adj, &mut path, &mut visited, end, &forbidden, n, min_len)
167}
168
169fn dfs_chordless(
170    adj: &[Vec<bool>],
171    path: &mut Vec<usize>,
172    visited: &mut Vec<bool>,
173    target: usize,
174    forbidden: &[bool],
175    n: usize,
176    min_len: usize,
177) -> bool {
178    let Some(&current) = path.last() else {
179        return false;
180    };
181    let depth = path.len();
182
183    // If current depth ≥ min_len and we can reach target
184    if depth >= min_len && adj[current][target] {
185        // Check chordlessness: no internal vertex adjacent to target
186        // except the last one (current).
187        // Actually we need: no chord in the cycle. The cycle is
188        // excluded - start - path - target - excluded.
189        // We already ensured internal vertices are not neighbors of
190        // excluded. We need to check that no internal vertex of the
191        // path (indices 1..depth-1) is adjacent to target.
192        let chordless = path[1..depth - 1].iter().all(|&v| !adj[v][target]);
193        if chordless {
194            return true;
195        }
196    }
197
198    // Limit search depth to avoid exponential blowup
199    if depth >= n.min(12) {
200        return false;
201    }
202
203    for next in 0..n {
204        if visited[next] || forbidden[next] || next == target && depth < min_len - 1 {
205            continue;
206        }
207        if !adj[current][next] {
208            continue;
209        }
210
211        // Check chordlessness: next must not be adjacent to any
212        // path vertex except current (the immediately previous one)
213        let has_chord = path[..depth - 1].iter().any(|&v| adj[next][v]);
214        if has_chord {
215            continue;
216        }
217
218        visited[next] = true;
219        path.push(next);
220        if dfs_chordless(adj, path, visited, target, forbidden, n, min_len) {
221            return true;
222        }
223        path.pop();
224        visited[next] = false;
225    }
226
227    false
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use crate::algorithms::chordality::is_chordal;
234
235    #[test]
236    fn empty_graph() {
237        let g = Graph::with_vertices(0);
238        assert!(is_weakly_chordal(&g).unwrap());
239    }
240
241    #[test]
242    fn single_vertex() {
243        let g = Graph::with_vertices(1);
244        assert!(is_weakly_chordal(&g).unwrap());
245    }
246
247    #[test]
248    fn single_edge() {
249        let mut g = Graph::with_vertices(2);
250        g.add_edge(0, 1).unwrap();
251        assert!(is_weakly_chordal(&g).unwrap());
252    }
253
254    #[test]
255    fn triangle() {
256        let mut g = Graph::with_vertices(3);
257        g.add_edge(0, 1).unwrap();
258        g.add_edge(1, 2).unwrap();
259        g.add_edge(2, 0).unwrap();
260        assert!(is_weakly_chordal(&g).unwrap());
261    }
262
263    #[test]
264    fn c4_weakly_chordal() {
265        // C_4 is weakly chordal (induced C_4 is allowed, only C_5+ banned)
266        let mut g = Graph::with_vertices(4);
267        g.add_edge(0, 1).unwrap();
268        g.add_edge(1, 2).unwrap();
269        g.add_edge(2, 3).unwrap();
270        g.add_edge(3, 0).unwrap();
271        assert!(is_weakly_chordal(&g).unwrap());
272    }
273
274    #[test]
275    fn c5_not_weakly_chordal() {
276        let mut g = Graph::with_vertices(5);
277        g.add_edge(0, 1).unwrap();
278        g.add_edge(1, 2).unwrap();
279        g.add_edge(2, 3).unwrap();
280        g.add_edge(3, 4).unwrap();
281        g.add_edge(4, 0).unwrap();
282        assert!(!is_weakly_chordal(&g).unwrap());
283    }
284
285    #[test]
286    fn c6_not_weakly_chordal() {
287        let mut g = Graph::with_vertices(6);
288        for i in 0..6u32 {
289            g.add_edge(i, (i + 1) % 6).unwrap();
290        }
291        assert!(!is_weakly_chordal(&g).unwrap());
292    }
293
294    #[test]
295    fn complete_k5() {
296        // Chordal → weakly chordal
297        let mut g = Graph::with_vertices(5);
298        for i in 0..5u32 {
299            for j in (i + 1)..5 {
300                g.add_edge(i, j).unwrap();
301            }
302        }
303        assert!(is_weakly_chordal(&g).unwrap());
304    }
305
306    #[test]
307    fn chordal_graph() {
308        // Any chordal graph is weakly chordal
309        let mut g = Graph::with_vertices(5);
310        g.add_edge(0, 1).unwrap();
311        g.add_edge(0, 2).unwrap();
312        g.add_edge(1, 2).unwrap();
313        g.add_edge(2, 3).unwrap();
314        g.add_edge(2, 4).unwrap();
315        g.add_edge(3, 4).unwrap();
316        assert!(is_chordal(&g, None).unwrap().chordal);
317        assert!(is_weakly_chordal(&g).unwrap());
318    }
319
320    #[test]
321    fn edgeless() {
322        // Complement of 5 isolated vertices is K_5 (chordal) → weakly chordal
323        let g = Graph::with_vertices(5);
324        assert!(is_weakly_chordal(&g).unwrap());
325    }
326
327    #[test]
328    fn path_p5() {
329        // P_5 is a tree → no induced cycle in G.
330        // Complement edges: 0-2,0-3,0-4,1-3,1-4,2-4.
331        // Cycle 0-2-4-1-3-0 has chord 0-4, so no induced C_5.
332        // → P_5 is weakly chordal.
333        let mut g = Graph::with_vertices(5);
334        g.add_edge(0, 1).unwrap();
335        g.add_edge(1, 2).unwrap();
336        g.add_edge(2, 3).unwrap();
337        g.add_edge(3, 4).unwrap();
338        assert!(is_weakly_chordal(&g).unwrap());
339    }
340
341    #[test]
342    fn star() {
343        // Star S_4: center 0, leaves 1,2,3,4.
344        // G is a tree → chordal → no induced C_5+ in G.
345        // Complement: 0 isolated, 1-2-3-4 complete (K_4).
346        // K_4 is chordal → no induced C_5+ in complement.
347        // → weakly chordal
348        let mut g = Graph::with_vertices(5);
349        g.add_edge(0, 1).unwrap();
350        g.add_edge(0, 2).unwrap();
351        g.add_edge(0, 3).unwrap();
352        g.add_edge(0, 4).unwrap();
353        assert!(is_weakly_chordal(&g).unwrap());
354    }
355
356    #[test]
357    fn diamond() {
358        let mut g = Graph::with_vertices(4);
359        g.add_edge(0, 1).unwrap();
360        g.add_edge(0, 2).unwrap();
361        g.add_edge(0, 3).unwrap();
362        g.add_edge(1, 2).unwrap();
363        g.add_edge(1, 3).unwrap();
364        assert!(is_weakly_chordal(&g).unwrap());
365    }
366
367    #[test]
368    fn directed_treated_as_undirected() {
369        let mut g = Graph::new(4, true).unwrap();
370        g.add_edge(0, 1).unwrap();
371        g.add_edge(1, 2).unwrap();
372        g.add_edge(2, 3).unwrap();
373        g.add_edge(3, 0).unwrap();
374        assert!(is_weakly_chordal(&g).unwrap());
375    }
376}