Skip to main content

scirs2_optimize/combinatorial/
graph_coloring.rs

1//! Graph coloring algorithms.
2//!
3//! Provides greedy (Welsh-Powell), DSATUR, exact backtracking, and chromatic
4//! number computation for undirected graphs.
5
6use crate::error::OptimizeError;
7
8/// Result type for graph coloring operations.
9pub type ColoringResult<T> = Result<T, OptimizeError>;
10
11// ── Graph structure ───────────────────────────────────────────────────────────
12
13/// Undirected graph represented as an adjacency list.
14#[derive(Debug, Clone)]
15pub struct GraphColoring {
16    /// Adjacency list: `adj[u]` contains all neighbours of `u`.
17    pub adj: Vec<Vec<usize>>,
18    /// Number of vertices.
19    pub n: usize,
20}
21
22impl GraphColoring {
23    /// Create a new empty graph with `n` vertices.
24    pub fn new(n: usize) -> Self {
25        Self {
26            adj: vec![vec![]; n],
27            n,
28        }
29    }
30
31    /// Add an undirected edge between `u` and `v`.
32    ///
33    /// Duplicate edges are silently ignored.
34    ///
35    /// # Errors
36    /// Returns an error if `u` or `v` are out of range.
37    pub fn add_edge(&mut self, u: usize, v: usize) -> ColoringResult<()> {
38        if u >= self.n || v >= self.n {
39            return Err(OptimizeError::InvalidInput(format!(
40                "Edge ({u},{v}) out of range for graph with {} vertices",
41                self.n
42            )));
43        }
44        if u == v {
45            return Ok(()); // ignore self-loops
46        }
47        if !self.adj[u].contains(&v) {
48            self.adj[u].push(v);
49        }
50        if !self.adj[v].contains(&u) {
51            self.adj[v].push(u);
52        }
53        Ok(())
54    }
55
56    /// Degree of vertex `v`.
57    pub fn degree(&self, v: usize) -> usize {
58        self.adj[v].len()
59    }
60
61    // ── Welsh-Powell greedy coloring ──────────────────────────────────────────
62
63    /// Greedy coloring using the Welsh-Powell heuristic (sort by degree desc).
64    ///
65    /// Assigns the smallest available color to each vertex in degree-descending
66    /// order.  Returns a color array where `coloring[v]` is the color (0-indexed)
67    /// assigned to vertex `v`.
68    pub fn greedy_coloring(&self) -> Vec<usize> {
69        if self.n == 0 {
70            return vec![];
71        }
72
73        // Sort vertices by degree descending
74        let mut order: Vec<usize> = (0..self.n).collect();
75        order.sort_by(|&a, &b| self.degree(b).cmp(&self.degree(a)));
76
77        let mut color = vec![usize::MAX; self.n];
78
79        for &v in &order {
80            let mut neighbor_colors = std::collections::HashSet::new();
81            for &u in &self.adj[v] {
82                if color[u] != usize::MAX {
83                    neighbor_colors.insert(color[u]);
84                }
85            }
86            // Assign smallest available color
87            let mut c = 0;
88            while neighbor_colors.contains(&c) {
89                c += 1;
90            }
91            color[v] = c;
92        }
93
94        color
95    }
96
97    // ── DSATUR coloring ───────────────────────────────────────────────────────
98
99    /// DSATUR coloring: at each step, color the vertex with maximum saturation
100    /// (number of distinct colors among already-colored neighbours), breaking
101    /// ties by degree.
102    ///
103    /// Often produces optimal or near-optimal colorings.
104    pub fn dsatur_coloring(&self) -> Vec<usize> {
105        if self.n == 0 {
106            return vec![];
107        }
108
109        let mut color = vec![usize::MAX; self.n];
110        // saturation[v] = number of distinct colors in N(v) that are already assigned
111        let mut saturation = vec![0usize; self.n];
112        // neighbor_color_sets[v] = set of colors seen in N(v)
113        let mut neighbor_colors: Vec<std::collections::HashSet<usize>> =
114            vec![std::collections::HashSet::new(); self.n];
115        let mut colored = 0usize;
116
117        while colored < self.n {
118            // Pick uncolored vertex with max saturation, break ties by degree
119            // The loop condition (colored < self.n) guarantees an uncolored vertex exists.
120            let v = match (0..self.n)
121                .filter(|&u| color[u] == usize::MAX)
122                .max_by(|&a, &b| {
123                    saturation[a]
124                        .cmp(&saturation[b])
125                        .then(self.degree(a).cmp(&self.degree(b)))
126                }) {
127                Some(vertex) => vertex,
128                None => break, // all vertices colored (should not occur given loop condition)
129            };
130
131            // Assign smallest available color
132            let mut c = 0;
133            while neighbor_colors[v].contains(&c) {
134                c += 1;
135            }
136            color[v] = c;
137            colored += 1;
138
139            // Update saturation of uncolored neighbours
140            for &u in &self.adj[v] {
141                if color[u] == usize::MAX && !neighbor_colors[u].contains(&c) {
142                    neighbor_colors[u].insert(c);
143                    saturation[u] += 1;
144                }
145            }
146        }
147
148        color
149    }
150
151    // ── Backtracking exact k-coloring ─────────────────────────────────────────
152
153    /// Try to find a valid k-coloring via backtracking with forward checking.
154    ///
155    /// Returns `Some(coloring)` if a k-coloring exists, `None` otherwise.
156    pub fn backtrack_coloring(&self, k: usize) -> Option<Vec<usize>> {
157        if self.n == 0 {
158            return Some(vec![]);
159        }
160        if k == 0 {
161            return None;
162        }
163
164        let mut color = vec![k; self.n]; // k = uncolored sentinel
165                                         // Order by degree descending (Welsh-Powell ordering)
166        let mut order: Vec<usize> = (0..self.n).collect();
167        order.sort_by(|&a, &b| self.degree(b).cmp(&self.degree(a)));
168        let pos_of: Vec<usize> = {
169            let mut p = vec![0usize; self.n];
170            for (pos, &v) in order.iter().enumerate() {
171                p[v] = pos;
172            }
173            p
174        };
175
176        if self.backtrack_inner(&mut color, &order, &pos_of, k, 0) {
177            Some(color)
178        } else {
179            None
180        }
181    }
182
183    fn backtrack_inner(
184        &self,
185        color: &mut Vec<usize>,
186        order: &[usize],
187        pos_of: &[usize],
188        k: usize,
189        idx: usize,
190    ) -> bool {
191        if idx == self.n {
192            return true;
193        }
194        let v = order[idx];
195
196        // Collect forbidden colors
197        let mut forbidden = vec![false; k];
198        for &u in &self.adj[v] {
199            if color[u] < k {
200                // already colored
201                forbidden[color[u]] = true;
202            }
203        }
204
205        for c in 0..k {
206            if forbidden[c] {
207                continue;
208            }
209            // Forward checking: ensure all uncolored neighbours still have at
210            // least one available color after assigning c to v
211            let ok = self.adj[v].iter().all(|&u| {
212                if color[u] < k {
213                    return true; // already colored, no change
214                }
215                // u is uncolored; check if any color other than c is available
216                let used: std::collections::HashSet<usize> = self.adj[u]
217                    .iter()
218                    .filter(|&&w| color[w] < k && w != v)
219                    .map(|&w| color[w])
220                    .collect();
221                // u's forbidden set = used ∪ {c}
222                let available = (0..k).filter(|&x| x != c && !used.contains(&x)).count();
223                // Check neighbours that are later in order
224                if pos_of[u] > idx {
225                    available > 0
226                } else {
227                    true
228                }
229            });
230
231            if !ok {
232                continue;
233            }
234
235            color[v] = c;
236            if self.backtrack_inner(color, order, pos_of, k, idx + 1) {
237                return true;
238            }
239            color[v] = k; // uncolor
240        }
241
242        false
243    }
244
245    // ── Chromatic number ──────────────────────────────────────────────────────
246
247    /// Compute the chromatic number χ(G) via binary search + backtracking.
248    ///
249    /// The greedy coloring gives an upper bound; 1 is the lower bound (unless
250    /// there are edges, in which case 2 is the lower bound).
251    pub fn chromatic_number(&self) -> usize {
252        if self.n == 0 {
253            return 0;
254        }
255        // Lower bound: clique number lower bound via degree
256        let has_edges = self.adj.iter().any(|nbrs| !nbrs.is_empty());
257        let lower = if has_edges { 2 } else { 1 };
258
259        // Upper bound from DSATUR
260        let upper_coloring = self.dsatur_coloring();
261        let upper = upper_coloring
262            .iter()
263            .cloned()
264            .max()
265            .map(|m| m + 1)
266            .unwrap_or(1);
267
268        if lower >= upper {
269            return lower;
270        }
271
272        // Binary search in [lower, upper]
273        let mut lo = lower;
274        let mut hi = upper;
275        while lo < hi {
276            let mid = (lo + hi) / 2;
277            if self.backtrack_coloring(mid).is_some() {
278                hi = mid;
279            } else {
280                lo = mid + 1;
281            }
282        }
283        lo
284    }
285
286    // ── Validation ────────────────────────────────────────────────────────────
287
288    /// Check that `coloring` is a valid proper coloring (no two adjacent
289    /// vertices share the same color).
290    pub fn is_valid_coloring(&self, coloring: &[usize]) -> bool {
291        if coloring.len() != self.n {
292            return false;
293        }
294        for u in 0..self.n {
295            for &v in &self.adj[u] {
296                if coloring[u] == coloring[v] {
297                    return false;
298                }
299            }
300        }
301        true
302    }
303}
304
305// ─────────────────────────────────────────────────────────────────────────────
306// Tests
307// ─────────────────────────────────────────────────────────────────────────────
308#[cfg(test)]
309mod tests {
310    use super::*;
311
312    fn cycle_graph(n: usize) -> GraphColoring {
313        let mut g = GraphColoring::new(n);
314        for i in 0..n {
315            g.add_edge(i, (i + 1) % n).expect("unexpected None or Err");
316        }
317        g
318    }
319
320    fn complete_graph(n: usize) -> GraphColoring {
321        let mut g = GraphColoring::new(n);
322        for i in 0..n {
323            for j in i + 1..n {
324                g.add_edge(i, j).expect("unexpected None or Err");
325            }
326        }
327        g
328    }
329
330    #[test]
331    fn test_greedy_valid() {
332        let g = cycle_graph(6);
333        let c = g.greedy_coloring();
334        assert!(g.is_valid_coloring(&c));
335    }
336
337    #[test]
338    fn test_dsatur_valid() {
339        let g = complete_graph(5);
340        let c = g.dsatur_coloring();
341        assert!(g.is_valid_coloring(&c));
342        // K5 needs 5 colors
343        let num_colors = c
344            .iter()
345            .cloned()
346            .max()
347            .expect("failed to create num_colors")
348            + 1;
349        assert_eq!(num_colors, 5);
350    }
351
352    #[test]
353    fn test_bipartite_2_colorable() {
354        // Complete bipartite K3,3
355        let mut g = GraphColoring::new(6);
356        for u in 0..3 {
357            for v in 3..6 {
358                g.add_edge(u, v).expect("unexpected None or Err");
359            }
360        }
361        let c = g.backtrack_coloring(2);
362        assert!(c.is_some());
363        assert!(g.is_valid_coloring(c.as_ref().expect("unexpected None or Err")));
364    }
365
366    #[test]
367    fn test_odd_cycle_not_2_colorable() {
368        let g = cycle_graph(5); // C5 needs 3 colors
369        assert!(g.backtrack_coloring(2).is_none());
370        assert!(g.backtrack_coloring(3).is_some());
371    }
372
373    #[test]
374    fn test_chromatic_number_cycle() {
375        let even = cycle_graph(6);
376        assert_eq!(even.chromatic_number(), 2);
377
378        let odd = cycle_graph(5);
379        assert_eq!(odd.chromatic_number(), 3);
380    }
381
382    #[test]
383    fn test_chromatic_number_complete() {
384        for n in 1..=5 {
385            let g = complete_graph(n);
386            assert_eq!(g.chromatic_number(), n);
387        }
388    }
389
390    #[test]
391    fn test_empty_graph() {
392        let g = GraphColoring::new(4);
393        let c = g.greedy_coloring();
394        // All vertices get color 0 (no edges)
395        assert!(c.iter().all(|&x| x == 0));
396        assert!(g.is_valid_coloring(&c));
397        assert_eq!(g.chromatic_number(), 1);
398    }
399
400    #[test]
401    fn test_self_loop_ignored() {
402        let mut g = GraphColoring::new(3);
403        g.add_edge(0, 0).expect("unexpected None or Err"); // self-loop ignored
404        g.add_edge(0, 1).expect("unexpected None or Err");
405        assert_eq!(g.degree(0), 1);
406    }
407
408    #[test]
409    fn test_invalid_edge() {
410        let mut g = GraphColoring::new(3);
411        assert!(g.add_edge(0, 5).is_err());
412    }
413}