Skip to main content

rust_igraph/algorithms/properties/
treewidth.rs

1//! Graph treewidth (ALGO-TR-034).
2//!
3//! The **treewidth** of a graph measures how tree-like it is.
4//! Computing exact treewidth is NP-hard; we provide:
5//!
6//! - `treewidth_upper_bound`: greedy min-degree elimination ordering.
7//! - `treewidth_min_fill`: greedy min-fill elimination ordering.
8//! - `elimination_ordering`: the vertex ordering from min-degree heuristic.
9
10#![allow(
11    clippy::cast_possible_truncation,
12    clippy::cast_precision_loss,
13    clippy::many_single_char_names,
14    clippy::needless_range_loop,
15    clippy::too_many_lines
16)]
17
18use crate::core::{Graph, IgraphResult};
19
20/// Compute an upper bound on treewidth via min-degree elimination.
21///
22/// Repeatedly eliminates the vertex with minimum degree, connecting
23/// all its neighbours (creating a clique), and tracks the maximum
24/// degree at elimination time.
25///
26/// # Examples
27///
28/// ```
29/// use rust_igraph::{Graph, treewidth_upper_bound};
30///
31/// // Path graph: treewidth = 1
32/// let g = Graph::from_edges(&[(0,1),(1,2),(2,3)], false, Some(4)).unwrap();
33/// assert_eq!(treewidth_upper_bound(&g).unwrap(), 1);
34/// ```
35pub fn treewidth_upper_bound(graph: &Graph) -> IgraphResult<u32> {
36    let (tw, _) = elimination_order_internal(graph, EliminationHeuristic::MinDegree);
37    Ok(tw)
38}
39
40/// Compute an upper bound on treewidth via min-fill elimination.
41///
42/// Repeatedly eliminates the vertex that would introduce the fewest
43/// new edges (fill edges) when eliminated.
44///
45/// # Examples
46///
47/// ```
48/// use rust_igraph::{Graph, treewidth_min_fill};
49///
50/// let g = Graph::from_edges(&[(0,1),(1,2),(2,3)], false, Some(4)).unwrap();
51/// assert_eq!(treewidth_min_fill(&g).unwrap(), 1);
52/// ```
53pub fn treewidth_min_fill(graph: &Graph) -> IgraphResult<u32> {
54    let (tw, _) = elimination_order_internal(graph, EliminationHeuristic::MinFill);
55    Ok(tw)
56}
57
58/// Compute an elimination ordering using the min-degree heuristic.
59///
60/// Returns the ordering as a sequence of vertex ids.
61///
62/// # Examples
63///
64/// ```
65/// use rust_igraph::{Graph, elimination_ordering};
66///
67/// let g = Graph::from_edges(&[(0,1),(1,2),(2,3)], false, Some(4)).unwrap();
68/// let order = elimination_ordering(&g).unwrap();
69/// assert_eq!(order.len(), 4);
70/// ```
71pub fn elimination_ordering(graph: &Graph) -> IgraphResult<Vec<u32>> {
72    let (_, order) = elimination_order_internal(graph, EliminationHeuristic::MinDegree);
73    Ok(order)
74}
75
76#[derive(Clone, Copy)]
77enum EliminationHeuristic {
78    MinDegree,
79    MinFill,
80}
81
82fn elimination_order_internal(graph: &Graph, heuristic: EliminationHeuristic) -> (u32, Vec<u32>) {
83    let n = graph.vcount() as usize;
84    if n == 0 {
85        return (0, Vec::new());
86    }
87
88    let mut adj: Vec<Vec<bool>> = vec![vec![false; n]; n];
89    for (u, v) in graph.edges() {
90        let ui = u as usize;
91        let vi = v as usize;
92        if ui != vi {
93            adj[ui][vi] = true;
94            if !graph.is_directed() {
95                adj[vi][ui] = true;
96            }
97        }
98    }
99
100    let mut eliminated = vec![false; n];
101    let mut order = Vec::with_capacity(n);
102    let mut tw: u32 = 0;
103
104    for _ in 0..n {
105        let v = match heuristic {
106            EliminationHeuristic::MinDegree => pick_min_degree(&adj, &eliminated, n),
107            EliminationHeuristic::MinFill => pick_min_fill(&adj, &eliminated, n),
108        };
109
110        let nbrs: Vec<usize> = (0..n)
111            .filter(|&u| !eliminated[u] && u != v && adj[v][u])
112            .collect();
113
114        let deg = nbrs.len() as u32;
115        if deg > tw {
116            tw = deg;
117        }
118
119        for i in 0..nbrs.len() {
120            for j in (i + 1)..nbrs.len() {
121                let a = nbrs[i];
122                let b = nbrs[j];
123                adj[a][b] = true;
124                adj[b][a] = true;
125            }
126        }
127
128        eliminated[v] = true;
129        order.push(v as u32);
130    }
131
132    (tw, order)
133}
134
135fn pick_min_degree(adj: &[Vec<bool>], eliminated: &[bool], n: usize) -> usize {
136    let mut best = usize::MAX;
137    let mut best_v = 0;
138
139    for v in 0..n {
140        if eliminated[v] {
141            continue;
142        }
143        let deg = (0..n)
144            .filter(|&u| !eliminated[u] && u != v && adj[v][u])
145            .count();
146        if deg < best {
147            best = deg;
148            best_v = v;
149        }
150    }
151
152    best_v
153}
154
155fn pick_min_fill(adj: &[Vec<bool>], eliminated: &[bool], n: usize) -> usize {
156    let mut best_fill = usize::MAX;
157    let mut best_deg = usize::MAX;
158    let mut best_v = 0;
159
160    for v in 0..n {
161        if eliminated[v] {
162            continue;
163        }
164        let nbrs: Vec<usize> = (0..n)
165            .filter(|&u| !eliminated[u] && u != v && adj[v][u])
166            .collect();
167
168        let mut fill: usize = 0;
169        for i in 0..nbrs.len() {
170            for j in (i + 1)..nbrs.len() {
171                if !adj[nbrs[i]][nbrs[j]] {
172                    fill = fill.saturating_add(1);
173                }
174            }
175        }
176
177        if fill < best_fill || (fill == best_fill && nbrs.len() < best_deg) {
178            best_fill = fill;
179            best_deg = nbrs.len();
180            best_v = v;
181        }
182    }
183
184    best_v
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    fn path4() -> Graph {
192        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
193    }
194
195    fn k3() -> Graph {
196        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
197    }
198
199    fn k4() -> Graph {
200        Graph::from_edges(
201            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
202            false,
203            Some(4),
204        )
205        .unwrap()
206    }
207
208    fn cycle4() -> Graph {
209        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
210    }
211
212    fn cycle5() -> Graph {
213        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
214    }
215
216    fn star5() -> Graph {
217        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
218    }
219
220    fn grid2x3() -> Graph {
221        // 0-1-2
222        // |   |   |
223        // 3-4-5
224        Graph::from_edges(
225            &[(0, 1), (1, 2), (3, 4), (4, 5), (0, 3), (1, 4), (2, 5)],
226            false,
227            Some(6),
228        )
229        .unwrap()
230    }
231
232    fn petersen() -> Graph {
233        Graph::from_edges(
234            &[
235                (0, 1),
236                (1, 2),
237                (2, 3),
238                (3, 4),
239                (4, 0),
240                (0, 5),
241                (1, 6),
242                (2, 7),
243                (3, 8),
244                (4, 9),
245                (5, 7),
246                (7, 9),
247                (9, 6),
248                (6, 8),
249                (8, 5),
250            ],
251            false,
252            Some(10),
253        )
254        .unwrap()
255    }
256
257    // --- treewidth_upper_bound ---
258
259    #[test]
260    fn twub_empty() {
261        let g = Graph::with_vertices(0);
262        assert_eq!(treewidth_upper_bound(&g).unwrap(), 0);
263    }
264
265    #[test]
266    fn twub_single() {
267        let g = Graph::with_vertices(1);
268        assert_eq!(treewidth_upper_bound(&g).unwrap(), 0);
269    }
270
271    #[test]
272    fn twub_no_edges() {
273        let g = Graph::with_vertices(5);
274        assert_eq!(treewidth_upper_bound(&g).unwrap(), 0);
275    }
276
277    #[test]
278    fn twub_path4() {
279        // Trees have treewidth 1
280        assert_eq!(treewidth_upper_bound(&path4()).unwrap(), 1);
281    }
282
283    #[test]
284    fn twub_star5() {
285        // Stars are trees — treewidth 1
286        assert_eq!(treewidth_upper_bound(&star5()).unwrap(), 1);
287    }
288
289    #[test]
290    fn twub_k3() {
291        // K_3: treewidth = 2
292        assert_eq!(treewidth_upper_bound(&k3()).unwrap(), 2);
293    }
294
295    #[test]
296    fn twub_k4() {
297        // K_4: treewidth = 3
298        assert_eq!(treewidth_upper_bound(&k4()).unwrap(), 3);
299    }
300
301    #[test]
302    fn twub_cycle4() {
303        // Cycles: treewidth = 2
304        assert_eq!(treewidth_upper_bound(&cycle4()).unwrap(), 2);
305    }
306
307    #[test]
308    fn twub_cycle5() {
309        assert_eq!(treewidth_upper_bound(&cycle5()).unwrap(), 2);
310    }
311
312    #[test]
313    fn twub_grid2x3() {
314        // 2x3 grid: treewidth = 2
315        let tw = treewidth_upper_bound(&grid2x3()).unwrap();
316        assert!(tw >= 2);
317        assert!(tw <= 3);
318    }
319
320    #[test]
321    fn twub_petersen() {
322        // Petersen: treewidth = 4
323        let tw = treewidth_upper_bound(&petersen()).unwrap();
324        assert!(tw >= 4);
325        assert!(tw <= 6);
326    }
327
328    // --- treewidth_min_fill ---
329
330    #[test]
331    fn twmf_path4() {
332        assert_eq!(treewidth_min_fill(&path4()).unwrap(), 1);
333    }
334
335    #[test]
336    fn twmf_k3() {
337        assert_eq!(treewidth_min_fill(&k3()).unwrap(), 2);
338    }
339
340    #[test]
341    fn twmf_k4() {
342        assert_eq!(treewidth_min_fill(&k4()).unwrap(), 3);
343    }
344
345    #[test]
346    fn twmf_cycle5() {
347        assert_eq!(treewidth_min_fill(&cycle5()).unwrap(), 2);
348    }
349
350    #[test]
351    fn twmf_star5() {
352        assert_eq!(treewidth_min_fill(&star5()).unwrap(), 1);
353    }
354
355    // --- elimination_ordering ---
356
357    #[test]
358    fn eo_length() {
359        let g = path4();
360        let order = elimination_ordering(&g).unwrap();
361        assert_eq!(order.len(), 4);
362    }
363
364    #[test]
365    fn eo_permutation() {
366        let g = k4();
367        let order = elimination_ordering(&g).unwrap();
368        let mut sorted = order.clone();
369        sorted.sort_unstable();
370        assert_eq!(sorted, vec![0, 1, 2, 3]);
371    }
372
373    #[test]
374    fn eo_empty() {
375        let g = Graph::with_vertices(0);
376        assert!(elimination_ordering(&g).unwrap().is_empty());
377    }
378
379    // --- cross-consistency ---
380
381    #[test]
382    fn min_fill_leq_min_degree() {
383        // min-fill is often tighter than min-degree
384        for g in &[path4(), k3(), cycle4(), cycle5(), star5()] {
385            let bound_degree = treewidth_upper_bound(g).unwrap();
386            let bound_fill = treewidth_min_fill(g).unwrap();
387            // Both are upper bounds; min-fill often matches or beats min-degree
388            assert!(bound_fill <= bound_degree.saturating_add(1));
389        }
390    }
391
392    #[test]
393    fn tw_at_least_one_for_edges() {
394        for g in &[path4(), k3(), k4(), cycle4(), cycle5()] {
395            assert!(treewidth_upper_bound(g).unwrap() >= 1);
396        }
397    }
398
399    #[test]
400    fn tw_leq_n_minus_1() {
401        for g in &[path4(), k3(), k4(), cycle5(), star5()] {
402            let n = g.vcount();
403            let tw = treewidth_upper_bound(g).unwrap();
404            assert!(tw < n);
405        }
406    }
407
408    #[test]
409    fn complete_graph_tw_is_n_minus_1() {
410        // treewidth(K_n) = n-1
411        for n in 2_u32..=5 {
412            let edges: Vec<(u32, u32)> = (0..n)
413                .flat_map(|i| ((i + 1)..n).map(move |j| (i, j)))
414                .collect();
415            let g = Graph::from_edges(&edges, false, Some(n)).unwrap();
416            assert_eq!(
417                treewidth_upper_bound(&g).unwrap(),
418                n - 1,
419                "tw(K_{n}) should be {}",
420                n - 1
421            );
422        }
423    }
424
425    #[test]
426    fn tree_tw_is_one() {
427        // Any tree has treewidth 1
428        let trees = vec![
429            path4(),
430            star5(),
431            Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (2, 4)], false, Some(5)).unwrap(),
432        ];
433        for g in &trees {
434            assert_eq!(treewidth_upper_bound(g).unwrap(), 1);
435        }
436    }
437}