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(¤t) = 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}