Skip to main content

rust_igraph/algorithms/
coloring.rs

1//! Graph coloring algorithms.
2//!
3//! ALGO-CL-001: `vertex_coloring_greedy` — heuristic greedy vertex coloring
4//! with two ordering strategies (DSATUR and Colored-Neighbors).
5//!
6//! Also provides three coloring validators:
7//! - [`is_vertex_coloring`] — check proper vertex coloring
8//! - [`is_bipartite_coloring`] — check bipartite (2-color) vertex coloring
9//! - [`is_edge_coloring`] — check proper edge coloring
10//!
11//! Reference: `references/igraph/src/misc/coloring.c` (519 lines).
12
13#![allow(
14    clippy::cast_possible_truncation,
15    clippy::cast_sign_loss,
16    clippy::too_many_lines,
17    clippy::similar_names,
18    clippy::module_name_repetitions
19)]
20
21use std::collections::BinaryHeap;
22
23use crate::IgraphResult;
24use crate::core::error::IgraphError;
25use crate::core::graph::{EdgeId, Graph, VertexId};
26
27/// Heuristic ordering strategy for [`vertex_coloring_greedy`].
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum GreedyColoringHeuristic {
30    /// Choose the uncolored vertex with the most already-colored neighbors.
31    /// Faster but typically uses more colors than DSATUR.
32    ColoredNeighbors,
33    /// Choose the uncolored vertex with the highest saturation degree
34    /// (number of distinct colors among neighbors), breaking ties by
35    /// degree in the uncolored subgraph. Generally produces better
36    /// (fewer-color) results.
37    DSatur,
38}
39
40/// Result of [`is_bipartite_coloring`].
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub struct BipartiteColoringResult {
43    /// Whether the coloring is a valid bipartite coloring.
44    pub valid: bool,
45    /// Edge direction mode (only meaningful when `valid` is true and
46    /// the graph is directed):
47    /// - `Some(BipartiteEdgeDirection::Out)` — all edges go from type-false to type-true
48    /// - `Some(BipartiteEdgeDirection::In)` — all edges go from type-true to type-false
49    /// - `Some(BipartiteEdgeDirection::All)` — edges go both ways, or graph is undirected
50    /// - `None` — coloring is invalid
51    pub mode: Option<BipartiteEdgeDirection>,
52}
53
54/// Edge direction pattern in a directed bipartite graph.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum BipartiteEdgeDirection {
57    /// All edges go from type-false to type-true vertices.
58    Out,
59    /// All edges go from type-true to type-false vertices.
60    In,
61    /// Edges go in both directions, or graph is undirected.
62    All,
63}
64
65// ---------------------------------------------------------------
66// vertex_coloring_greedy
67// ---------------------------------------------------------------
68
69/// Computes a vertex coloring using a greedy algorithm.
70///
71/// Assigns a color (non-negative integer starting at 0) to each vertex
72/// such that no two adjacent vertices share the same color. Self-loops
73/// are ignored. The result is not necessarily optimal (minimum-chromatic).
74///
75/// # Arguments
76///
77/// * `graph` — the input graph (directed or undirected; multi-edges and
78///   self-loops are tolerated).
79/// * `heuristic` — vertex ordering strategy; see [`GreedyColoringHeuristic`].
80///
81/// # Returns
82///
83/// A `Vec<u32>` of length `vcount`, where entry `i` is the color of vertex `i`.
84///
85/// # Examples
86///
87/// ```
88/// use rust_igraph::{vertex_coloring_greedy, GreedyColoringHeuristic, create};
89///
90/// // Triangle graph — needs at least 3 colors
91/// let g = create(&[(0, 1), (1, 2), (2, 0)], 3, false).unwrap();
92/// let colors = vertex_coloring_greedy(&g, GreedyColoringHeuristic::DSatur).unwrap();
93/// assert_eq!(colors.len(), 3);
94/// // All colors distinct
95/// assert_ne!(colors[0], colors[1]);
96/// assert_ne!(colors[1], colors[2]);
97/// assert_ne!(colors[0], colors[2]);
98/// ```
99pub fn vertex_coloring_greedy(
100    graph: &Graph,
101    heuristic: GreedyColoringHeuristic,
102) -> IgraphResult<Vec<u32>> {
103    match heuristic {
104        GreedyColoringHeuristic::ColoredNeighbors => greedy_cn(graph),
105        GreedyColoringHeuristic::DSatur => greedy_dsatur(graph),
106    }
107}
108
109/// All neighbors of `v` (both in and out for directed, merged for
110/// undirected). Includes self-loops and multi-edges.
111fn all_neighbors(graph: &Graph, v: VertexId) -> IgraphResult<Vec<VertexId>> {
112    if graph.is_directed() {
113        let mut out = graph.out_neighbors_vec(v)?;
114        let in_neis = graph.in_neighbors_vec(v)?;
115        out.extend(in_neis);
116        Ok(out)
117    } else {
118        Ok(graph.neighbors_iter(v)?.collect())
119    }
120}
121
122/// All neighbors of `v`, excluding self-loops and duplicate edges
123/// (simple neighbor list). Used by DSATUR.
124fn all_neighbors_simple(graph: &Graph, v: VertexId) -> IgraphResult<Vec<VertexId>> {
125    let mut neis = all_neighbors(graph, v)?;
126    neis.retain(|&n| n != v);
127    neis.sort_unstable();
128    neis.dedup();
129    Ok(neis)
130}
131
132/// `COLORED_NEIGHBORS` heuristic: pick the uncolored vertex with the
133/// most already-colored neighbors. Mirrors `igraph_i_vertex_coloring_greedy_cn`.
134fn greedy_cn(graph: &Graph) -> IgraphResult<Vec<u32>> {
135    let vc = graph.vcount() as usize;
136    let mut colors = vec![0u32; vc];
137
138    if vc <= 1 {
139        return Ok(colors);
140    }
141
142    // Find vertex with maximum degree.
143    let mut max_deg = 0usize;
144    let mut start_vertex: VertexId = 0;
145    for v in 0..graph.vcount() {
146        let d = graph.degree(v)?;
147        if d > max_deg {
148            max_deg = d;
149            start_vertex = v;
150        }
151    }
152
153    // colored_count[v] = number of already-colored neighbors of v.
154    let mut colored_count = vec![0u32; vc];
155    let mut colored = vec![false; vc];
156
157    // Lazy-deletion max-heap: (colored_count, vertex_id).
158    let mut heap: BinaryHeap<(u32, u32)> = BinaryHeap::with_capacity(vc);
159    for v in 0..graph.vcount() {
160        if v != start_vertex {
161            heap.push((0, v));
162        }
163    }
164
165    // Internally 1-based colors (0 = uncolored); subtract 1 at end.
166    let mut vertex = start_vertex;
167    loop {
168        let neis = all_neighbors(graph, vertex)?;
169
170        // Collect neighbor colors and find smallest available 1-based color.
171        let mut nei_cols: Vec<u32> = neis.iter().map(|&n| colors[n as usize]).collect();
172        nei_cols.sort_unstable();
173        let col = smallest_available_color_1based(&nei_cols);
174        colors[vertex as usize] = col;
175        colored[vertex as usize] = true;
176
177        // Update colored-neighbor counts and push new heap entries.
178        for &nei in &neis {
179            let ni = nei as usize;
180            if !colored[ni] {
181                colored_count[ni] = colored_count[ni].saturating_add(1);
182                heap.push((colored_count[ni], nei));
183            }
184        }
185
186        // Pop the next vertex (skip stale entries).
187        loop {
188            if let Some((count, v)) = heap.pop() {
189                if colored[v as usize] {
190                    continue;
191                }
192                if count < colored_count[v as usize] {
193                    continue;
194                }
195                vertex = v;
196                break;
197            }
198            // All vertices colored — subtract 1 to make 0-based.
199            for c in &mut colors {
200                *c -= 1;
201            }
202            return Ok(colors);
203        }
204    }
205}
206
207/// Find the smallest positive integer not present in a sorted array
208/// that may contain zeros. Used for 1-based color assignment.
209fn smallest_available_color_1based(sorted_colors: &[u32]) -> u32 {
210    let mut i = 0;
211    let mut col = 0u32;
212    let n = sorted_colors.len();
213    loop {
214        while i < n && sorted_colors[i] == col {
215            i += 1;
216        }
217        col += 1;
218        if i >= n || sorted_colors[i] != col {
219            break;
220        }
221    }
222    col
223}
224
225// ---------------------------------------------------------------
226// DSATUR heuristic
227// ---------------------------------------------------------------
228
229/// DSATUR key for heap ordering: (`saturation_degree`, `edge_degree_in_uncolored`).
230#[derive(Debug, Clone, Copy, PartialEq, Eq)]
231struct DsaturKey {
232    sat_deg: u32,
233    edge_deg: u32,
234}
235
236impl PartialOrd for DsaturKey {
237    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
238        Some(self.cmp(other))
239    }
240}
241
242impl Ord for DsaturKey {
243    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
244        self.sat_deg
245            .cmp(&other.sat_deg)
246            .then(self.edge_deg.cmp(&other.edge_deg))
247    }
248}
249
250/// DSATUR: choose vertices by saturation degree, breaking ties by
251/// degree in the uncolored subgraph. Mirrors `igraph_i_vertex_coloring_dsatur`.
252fn greedy_dsatur(graph: &Graph) -> IgraphResult<Vec<u32>> {
253    let vc = graph.vcount() as usize;
254
255    if vc == 0 {
256        return Ok(vec![]);
257    }
258    if vc == 1 {
259        return Ok(vec![0]);
260    }
261
262    let mut colors: Vec<Option<u32>> = vec![None; vc];
263
264    // Build simple adjacency list (no loops, no multi-edges).
265    let mut adj: Vec<Vec<VertexId>> = Vec::with_capacity(vc);
266    for v in 0..graph.vcount() {
267        adj.push(all_neighbors_simple(graph, v)?);
268    }
269
270    // DSATUR state per vertex.
271    let mut sat_deg = vec![0u32; vc];
272    let mut edge_deg = vec![0u32; vc];
273    for v in 0..vc {
274        edge_deg[v] = adj[v].len() as u32;
275    }
276    let mut remaining = vc;
277
278    // Lazy-deletion max-heap: (DsaturKey, vertex_id).
279    let mut heap: BinaryHeap<(DsaturKey, u32)> = BinaryHeap::with_capacity(vc);
280    for v in 0..graph.vcount() {
281        heap.push((
282            DsaturKey {
283                sat_deg: 0,
284                edge_deg: edge_deg[v as usize],
285            },
286            v,
287        ));
288    }
289
290    while remaining > 0 {
291        // Pop the highest-priority uncolored vertex.
292        let node_to_color = loop {
293            let (key, v) = heap.pop().ok_or_else(|| {
294                IgraphError::InvalidArgument("DSATUR heap empty unexpectedly".into())
295            })?;
296            if colors[v as usize].is_some() {
297                continue;
298            }
299            // Check if this entry is stale.
300            let cur_key = DsaturKey {
301                sat_deg: sat_deg[v as usize],
302                edge_deg: edge_deg[v as usize],
303            };
304            if key < cur_key {
305                continue;
306            }
307            break v;
308        };
309
310        // Find smallest available color among neighbors.
311        let neis = &adj[node_to_color as usize];
312        let mut used: Vec<u32> = neis.iter().filter_map(|&n| colors[n as usize]).collect();
313        used.sort_unstable();
314        let color = smallest_available_color_0based(&used);
315
316        colors[node_to_color as usize] = Some(color);
317        remaining -= 1;
318
319        // Update neighbors: for each uncolored neighbor, check if this
320        // new color increases its saturation degree, and decrement its
321        // edge_degree (one fewer uncolored neighbor).
322        for &nei in neis {
323            let ni = nei as usize;
324            if colors[ni].is_some() {
325                continue;
326            }
327
328            // Check if this color was already used by another colored
329            // neighbor of `nei`.
330            let color_already_used = adj[ni]
331                .iter()
332                .any(|&nn| nn != node_to_color && colors[nn as usize] == Some(color));
333
334            if !color_already_used {
335                sat_deg[ni] += 1;
336            }
337            edge_deg[ni] = edge_deg[ni].saturating_sub(1);
338
339            // Push updated priority.
340            heap.push((
341                DsaturKey {
342                    sat_deg: sat_deg[ni],
343                    edge_deg: edge_deg[ni],
344                },
345                nei,
346            ));
347        }
348    }
349
350    Ok(colors.into_iter().map(|c| c.unwrap_or(0)).collect())
351}
352
353/// Find the smallest non-negative integer not present in a sorted array
354/// of non-negative integers. Used for 0-based color assignment.
355fn smallest_available_color_0based(sorted: &[u32]) -> u32 {
356    let mut col = 0u32;
357    let mut i = 0;
358    let n = sorted.len();
359    while i < n && sorted[i] == col {
360        while i < n && sorted[i] == col {
361            i += 1;
362        }
363        col += 1;
364    }
365    col
366}
367
368// ---------------------------------------------------------------
369// is_vertex_coloring
370// ---------------------------------------------------------------
371
372/// Checks whether the given color assignment is a valid vertex coloring,
373/// i.e. no two adjacent vertices share the same color. Self-loops are
374/// ignored.
375///
376/// # Errors
377///
378/// Returns [`IgraphError::InvalidArgument`] if `colors.len() != vcount`.
379///
380/// # Examples
381///
382/// ```
383/// use rust_igraph::{is_vertex_coloring, create};
384///
385/// let g = create(&[(0, 1), (1, 2), (2, 0)], 3, false).unwrap();
386/// assert!(is_vertex_coloring(&g, &[0, 1, 2]).unwrap());
387/// assert!(!is_vertex_coloring(&g, &[0, 0, 1]).unwrap());
388/// ```
389pub fn is_vertex_coloring(graph: &Graph, colors: &[u32]) -> IgraphResult<bool> {
390    let vc = graph.vcount() as usize;
391    if colors.len() != vc {
392        return Err(IgraphError::InvalidArgument(format!(
393            "is_vertex_coloring: colors length {} != vcount {}",
394            colors.len(),
395            vc
396        )));
397    }
398
399    let ec = graph.ecount();
400    for e in 0..ec {
401        let eid = u32::try_from(e)
402            .map_err(|_| IgraphError::InvalidArgument("edge count exceeds u32".into()))?;
403        let (from, to) = graph.edge(eid)?;
404        if from == to {
405            continue;
406        }
407        if colors[from as usize] == colors[to as usize] {
408            return Ok(false);
409        }
410    }
411
412    Ok(true)
413}
414
415// ---------------------------------------------------------------
416// is_bipartite_coloring
417// ---------------------------------------------------------------
418
419/// Checks whether the given boolean type assignment is a valid bipartite
420/// coloring, i.e. no two adjacent vertices have the same type. Self-loops
421/// are ignored.
422///
423/// For directed graphs, also determines the edge direction mode:
424/// - `Out` — all edges go from type-false to type-true
425/// - `In` — all edges go from type-true to type-false
426/// - `All` — edges go both ways, or graph is undirected
427///
428/// # Errors
429///
430/// Returns [`IgraphError::InvalidArgument`] if `types.len() != vcount`.
431///
432/// # Examples
433///
434/// ```
435/// use rust_igraph::{is_bipartite_coloring, BipartiteEdgeDirection, create};
436///
437/// let g = create(&[(0, 2), (0, 3), (1, 2), (1, 3)], 4, false).unwrap();
438/// let result = is_bipartite_coloring(&g, &[false, false, true, true]).unwrap();
439/// assert!(result.valid);
440/// assert_eq!(result.mode, Some(BipartiteEdgeDirection::All));
441/// ```
442pub fn is_bipartite_coloring(
443    graph: &Graph,
444    types: &[bool],
445) -> IgraphResult<BipartiteColoringResult> {
446    let vc = graph.vcount() as usize;
447    if types.len() != vc {
448        return Err(IgraphError::InvalidArgument(format!(
449            "is_bipartite_coloring: types length {} != vcount {}",
450            types.len(),
451            vc
452        )));
453    }
454
455    let ec = graph.ecount();
456    let directed = graph.is_directed();
457    let mut has_false_to_true = false;
458    let mut has_true_to_false = false;
459
460    for e in 0..ec {
461        let eid = u32::try_from(e)
462            .map_err(|_| IgraphError::InvalidArgument("edge count exceeds u32".into()))?;
463        let (from, to) = graph.edge(eid)?;
464        if from == to {
465            continue;
466        }
467
468        let from_type = types[from as usize];
469        let to_type = types[to as usize];
470
471        if from_type == to_type {
472            return Ok(BipartiteColoringResult {
473                valid: false,
474                mode: None,
475            });
476        }
477
478        if directed {
479            if !from_type && to_type {
480                has_false_to_true = true;
481            } else if from_type && !to_type {
482                has_true_to_false = true;
483            }
484        }
485    }
486
487    let mode = if directed {
488        if has_false_to_true && !has_true_to_false {
489            BipartiteEdgeDirection::Out
490        } else if !has_false_to_true && has_true_to_false {
491            BipartiteEdgeDirection::In
492        } else {
493            BipartiteEdgeDirection::All
494        }
495    } else {
496        BipartiteEdgeDirection::All
497    };
498
499    Ok(BipartiteColoringResult {
500        valid: true,
501        mode: Some(mode),
502    })
503}
504
505// ---------------------------------------------------------------
506// is_edge_coloring
507// ---------------------------------------------------------------
508
509/// Checks whether the given edge color assignment is a valid edge
510/// coloring, i.e. no two edges sharing a vertex have the same color.
511/// Self-loops are not considered adjacent to themselves.
512///
513/// # Errors
514///
515/// Returns [`IgraphError::InvalidArgument`] if `colors.len() != ecount`.
516///
517/// # Examples
518///
519/// ```
520/// use rust_igraph::{is_edge_coloring, create};
521///
522/// let g = create(&[(0, 1), (1, 2), (2, 0)], 3, false).unwrap();
523/// assert!(is_edge_coloring(&g, &[0, 1, 2]).unwrap());
524/// assert!(!is_edge_coloring(&g, &[0, 1, 0]).unwrap()); // edges 0,2 share vertex 0
525/// ```
526pub fn is_edge_coloring(graph: &Graph, colors: &[u32]) -> IgraphResult<bool> {
527    let ec = graph.ecount();
528    if colors.len() != ec {
529        return Err(IgraphError::InvalidArgument(format!(
530            "is_edge_coloring: colors length {} != ecount {}",
531            colors.len(),
532            ec
533        )));
534    }
535
536    let vc = graph.vcount();
537
538    for v in 0..vc {
539        let edges = graph.incident(v)?;
540
541        // Dedup edge ids: self-loops appear twice in incident() for
542        // undirected graphs. We treat self-loops as NOT adjacent to
543        // themselves (matching igraph C using IGRAPH_LOOPS_ONCE).
544        let mut seen_eids: Vec<EdgeId> = Vec::with_capacity(edges.len());
545        for &eid in &edges {
546            if !seen_eids.contains(&eid) {
547                seen_eids.push(eid);
548            }
549        }
550
551        let mut edge_cols: Vec<u32> = seen_eids.iter().map(|&eid| colors[eid as usize]).collect();
552        edge_cols.sort_unstable();
553
554        for w in edge_cols.windows(2) {
555            if w[0] == w[1] {
556                return Ok(false);
557            }
558        }
559    }
560
561    Ok(true)
562}
563
564/// Compute a greedy edge coloring.
565///
566/// Assigns colors (starting from 0) to edges such that no two edges
567/// sharing a vertex receive the same color. Iterates edges in order
568/// and assigns the smallest color not used by any adjacent edge.
569///
570/// The greedy approach may use up to `2·Δ - 1` colors where `Δ` is
571/// the maximum degree. By Vizing's theorem, any simple graph can be
572/// edge-colored with at most `Δ + 1` colors, but the greedy
573/// heuristic does not guarantee that bound.
574///
575/// Self-loops are assigned a color but are not considered adjacent
576/// to themselves (matching `is_edge_coloring` behavior).
577///
578/// # Examples
579///
580/// ```
581/// use rust_igraph::{Graph, edge_coloring_greedy, is_edge_coloring};
582///
583/// let mut g = Graph::with_vertices(4);
584/// g.add_edge(0, 1).unwrap();
585/// g.add_edge(1, 2).unwrap();
586/// g.add_edge(2, 3).unwrap();
587/// g.add_edge(0, 2).unwrap();
588/// let colors = edge_coloring_greedy(&g).unwrap();
589/// assert!(is_edge_coloring(&g, &colors).unwrap());
590/// ```
591pub fn edge_coloring_greedy(graph: &Graph) -> IgraphResult<Vec<u32>> {
592    let ec = graph.ecount();
593
594    if ec == 0 {
595        return Ok(Vec::new());
596    }
597
598    let mut colors = vec![u32::MAX; ec];
599
600    // For each vertex, track which colors are used by incident edges
601    // We rebuild the used-color set per vertex on demand
602    for eid in 0..ec {
603        let (u, v) = graph.edge(eid as u32)?;
604
605        // Collect colors used by edges incident to u (excluding self: eid)
606        let mut used = Vec::new();
607        collect_used_colors(graph, u, eid, &colors, &mut used)?;
608        if u != v {
609            collect_used_colors(graph, v, eid, &colors, &mut used)?;
610        }
611        used.sort_unstable();
612        used.dedup();
613
614        // Find smallest unused color
615        let mut color = 0u32;
616        for &c in &used {
617            if c == color {
618                color = color.checked_add(1).unwrap_or(color);
619            } else {
620                break;
621            }
622        }
623
624        colors[eid] = color;
625    }
626
627    // Verify no u32::MAX remains (shouldn't happen for valid graphs)
628    debug_assert!(colors.iter().all(|&c| c != u32::MAX || ec == 0));
629
630    Ok(colors)
631}
632
633/// Return the number of distinct colors used by an edge coloring.
634///
635/// # Examples
636///
637/// ```
638/// use rust_igraph::{Graph, edge_coloring_greedy, edge_chromatic_number};
639///
640/// let mut g = Graph::with_vertices(3);
641/// g.add_edge(0, 1).unwrap();
642/// g.add_edge(1, 2).unwrap();
643/// g.add_edge(2, 0).unwrap();
644/// let colors = edge_coloring_greedy(&g).unwrap();
645/// assert_eq!(edge_chromatic_number(&colors), 3);
646/// ```
647pub fn edge_chromatic_number(colors: &[u32]) -> u32 {
648    if colors.is_empty() {
649        return 0;
650    }
651    let max_color = colors.iter().copied().max().unwrap_or(0);
652    max_color.checked_add(1).unwrap_or(max_color)
653}
654
655fn collect_used_colors(
656    graph: &Graph,
657    v: VertexId,
658    exclude_eid: usize,
659    colors: &[u32],
660    used: &mut Vec<u32>,
661) -> IgraphResult<()> {
662    let edges = graph.incident(v)?;
663    if graph.is_directed() {
664        let in_edges = graph.incident_in(v)?;
665        for &eid in edges.iter().chain(in_edges.iter()) {
666            if (eid as usize) != exclude_eid && colors[eid as usize] != u32::MAX {
667                used.push(colors[eid as usize]);
668            }
669        }
670    } else {
671        let mut seen: Vec<EdgeId> = Vec::with_capacity(edges.len());
672        for &eid in &edges {
673            if !seen.contains(&eid) {
674                seen.push(eid);
675                if (eid as usize) != exclude_eid && colors[eid as usize] != u32::MAX {
676                    used.push(colors[eid as usize]);
677                }
678            }
679        }
680    }
681    Ok(())
682}
683
684// ---------------------------------------------------------------
685// vertex_chromatic_number
686// ---------------------------------------------------------------
687
688/// Return the number of distinct colors used in a vertex coloring.
689///
690/// Given a color assignment `colors[v]` for each vertex, returns
691/// the count of distinct colors. This is a utility function that
692/// works on the output of [`vertex_coloring_greedy`].
693///
694/// For an empty coloring (no vertices), returns 0.
695///
696/// # Examples
697///
698/// ```
699/// use rust_igraph::{Graph, vertex_coloring_greedy, vertex_chromatic_number, GreedyColoringHeuristic};
700///
701/// let mut g = Graph::with_vertices(3);
702/// g.add_edge(0, 1).unwrap();
703/// g.add_edge(1, 2).unwrap();
704/// g.add_edge(2, 0).unwrap();
705/// let colors = vertex_coloring_greedy(&g, GreedyColoringHeuristic::DSatur).unwrap();
706/// assert_eq!(vertex_chromatic_number(&colors), 3); // triangle needs 3 colors
707/// ```
708pub fn vertex_chromatic_number(colors: &[u32]) -> u32 {
709    if colors.is_empty() {
710        return 0;
711    }
712    let max_color = colors.iter().copied().max().unwrap_or(0);
713    max_color.saturating_add(1)
714}
715
716/// Compute an upper bound on the vertex chromatic number using
717/// greedy `DSatur` coloring.
718///
719/// This is a convenience function that runs [`vertex_coloring_greedy`]
720/// with [`GreedyColoringHeuristic::DSatur`] and returns the number
721/// of colors used. The result is an upper bound on the true chromatic
722/// number χ(G).
723///
724/// # Examples
725///
726/// ```
727/// use rust_igraph::{Graph, chromatic_number_upper_bound};
728///
729/// // C4 is bipartite → χ = 2
730/// let mut g = Graph::with_vertices(4);
731/// g.add_edge(0, 1).unwrap();
732/// g.add_edge(1, 2).unwrap();
733/// g.add_edge(2, 3).unwrap();
734/// g.add_edge(3, 0).unwrap();
735/// assert_eq!(chromatic_number_upper_bound(&g).unwrap(), 2);
736///
737/// // K4 → χ = 4, DSatur finds optimal
738/// let mut g = Graph::with_vertices(4);
739/// for u in 0..4u32 {
740///     for v in (u + 1)..4 {
741///         g.add_edge(u, v).unwrap();
742///     }
743/// }
744/// assert_eq!(chromatic_number_upper_bound(&g).unwrap(), 4);
745/// ```
746pub fn chromatic_number_upper_bound(graph: &Graph) -> IgraphResult<u32> {
747    let colors = vertex_coloring_greedy(graph, GreedyColoringHeuristic::DSatur)?;
748    Ok(vertex_chromatic_number(&colors))
749}
750
751// =================================================================
752// Tests
753// =================================================================
754
755#[cfg(test)]
756mod tests {
757    use super::*;
758    use crate::create;
759
760    fn make_undirected(n: u32, edges: &[(u32, u32)]) -> Graph {
761        create(edges, n, false).expect("make_undirected")
762    }
763
764    fn make_directed(n: u32, edges: &[(u32, u32)]) -> Graph {
765        create(edges, n, true).expect("make_directed")
766    }
767
768    // ---- vertex_coloring_greedy ----
769
770    #[test]
771    fn test_empty_graph_cn() {
772        let g = make_undirected(0, &[]);
773        let c = vertex_coloring_greedy(&g, GreedyColoringHeuristic::ColoredNeighbors).expect("ok");
774        assert!(c.is_empty());
775    }
776
777    #[test]
778    fn test_empty_graph_dsatur() {
779        let g = make_undirected(0, &[]);
780        let c = vertex_coloring_greedy(&g, GreedyColoringHeuristic::DSatur).expect("ok");
781        assert!(c.is_empty());
782    }
783
784    #[test]
785    fn test_singleton_cn() {
786        let g = make_undirected(1, &[]);
787        let c = vertex_coloring_greedy(&g, GreedyColoringHeuristic::ColoredNeighbors).expect("ok");
788        assert_eq!(c, vec![0]);
789    }
790
791    #[test]
792    fn test_singleton_dsatur() {
793        let g = make_undirected(1, &[]);
794        let c = vertex_coloring_greedy(&g, GreedyColoringHeuristic::DSatur).expect("ok");
795        assert_eq!(c, vec![0]);
796    }
797
798    #[test]
799    fn test_triangle_cn() {
800        let g = make_undirected(3, &[(0, 1), (1, 2), (2, 0)]);
801        let c = vertex_coloring_greedy(&g, GreedyColoringHeuristic::ColoredNeighbors).expect("ok");
802        assert_eq!(c.len(), 3);
803        assert!(is_vertex_coloring(&g, &c).expect("validate"));
804        let max_color = c.iter().copied().max().unwrap_or(0);
805        assert!(max_color >= 2, "triangle needs at least 3 colors");
806    }
807
808    #[test]
809    fn test_triangle_dsatur() {
810        let g = make_undirected(3, &[(0, 1), (1, 2), (2, 0)]);
811        let c = vertex_coloring_greedy(&g, GreedyColoringHeuristic::DSatur).expect("ok");
812        assert_eq!(c.len(), 3);
813        assert!(is_vertex_coloring(&g, &c).expect("validate"));
814        let num_colors = c.iter().copied().max().unwrap_or(0) + 1;
815        assert_eq!(num_colors, 3);
816    }
817
818    #[test]
819    fn test_isolated_vertices_cn() {
820        let g = make_undirected(4, &[(0, 1)]);
821        let c = vertex_coloring_greedy(&g, GreedyColoringHeuristic::ColoredNeighbors).expect("ok");
822        assert!(is_vertex_coloring(&g, &c).expect("validate"));
823        assert_eq!(c[2], 0);
824        assert_eq!(c[3], 0);
825    }
826
827    #[test]
828    fn test_isolated_vertices_dsatur() {
829        let g = make_undirected(4, &[(0, 1)]);
830        let c = vertex_coloring_greedy(&g, GreedyColoringHeuristic::DSatur).expect("ok");
831        assert!(is_vertex_coloring(&g, &c).expect("validate"));
832        assert_eq!(c[2], 0);
833        assert_eq!(c[3], 0);
834    }
835
836    #[test]
837    fn test_self_loops_cn() {
838        let g = make_undirected(3, &[(0, 0), (2, 2), (2, 2)]);
839        let c = vertex_coloring_greedy(&g, GreedyColoringHeuristic::ColoredNeighbors).expect("ok");
840        assert!(is_vertex_coloring(&g, &c).expect("validate"));
841        let num_colors = c.iter().copied().max().unwrap_or(0) + 1;
842        assert_eq!(num_colors, 1, "only self-loops, 1 color suffices");
843    }
844
845    #[test]
846    fn test_self_loops_dsatur() {
847        let g = make_undirected(3, &[(0, 0), (2, 2), (2, 2)]);
848        let c = vertex_coloring_greedy(&g, GreedyColoringHeuristic::DSatur).expect("ok");
849        assert!(is_vertex_coloring(&g, &c).expect("validate"));
850        let num_colors = c.iter().copied().max().unwrap_or(0) + 1;
851        assert_eq!(num_colors, 1);
852    }
853
854    #[test]
855    fn test_bipartite_graph_dsatur_optimal() {
856        let g = make_undirected(
857            6,
858            &[
859                (0, 3),
860                (0, 4),
861                (0, 5),
862                (1, 3),
863                (1, 4),
864                (1, 5),
865                (2, 3),
866                (2, 4),
867                (2, 5),
868            ],
869        );
870        let c = vertex_coloring_greedy(&g, GreedyColoringHeuristic::DSatur).expect("ok");
871        assert!(is_vertex_coloring(&g, &c).expect("validate"));
872        let num_colors = c.iter().copied().max().unwrap_or(0) + 1;
873        assert_eq!(num_colors, 2);
874    }
875
876    #[test]
877    fn test_wheel_odd_dsatur() {
878        let mut edges: Vec<(u32, u32)> = Vec::new();
879        for i in 1..=10u32 {
880            edges.push((0, i));
881        }
882        for i in 1..10u32 {
883            edges.push((i, i + 1));
884        }
885        edges.push((10, 1));
886        let g = make_undirected(11, &edges);
887        let c = vertex_coloring_greedy(&g, GreedyColoringHeuristic::DSatur).expect("ok");
888        assert!(is_vertex_coloring(&g, &c).expect("validate"));
889        let num_colors = c.iter().copied().max().unwrap_or(0) + 1;
890        assert_eq!(num_colors, 3);
891    }
892
893    #[test]
894    fn test_wheel_even_dsatur() {
895        let mut edges: Vec<(u32, u32)> = Vec::new();
896        for i in 1..=11u32 {
897            edges.push((0, i));
898        }
899        for i in 1..11u32 {
900            edges.push((i, i + 1));
901        }
902        edges.push((11, 1));
903        let g = make_undirected(12, &edges);
904        let c = vertex_coloring_greedy(&g, GreedyColoringHeuristic::DSatur).expect("ok");
905        assert!(is_vertex_coloring(&g, &c).expect("validate"));
906        let num_colors = c.iter().copied().max().unwrap_or(0) + 1;
907        assert_eq!(num_colors, 4);
908    }
909
910    #[test]
911    fn test_small_graph_both_heuristics() {
912        let g = make_undirected(
913            8,
914            &[
915                (0, 3),
916                (0, 4),
917                (1, 4),
918                (2, 4),
919                (1, 5),
920                (2, 5),
921                (3, 5),
922                (0, 6),
923                (1, 6),
924                (2, 6),
925                (3, 6),
926                (0, 7),
927                (1, 7),
928                (2, 7),
929                (4, 7),
930                (5, 7),
931            ],
932        );
933
934        for heuristic in [
935            GreedyColoringHeuristic::ColoredNeighbors,
936            GreedyColoringHeuristic::DSatur,
937        ] {
938            let c = vertex_coloring_greedy(&g, heuristic).expect("ok");
939            assert_eq!(c.len(), 8);
940            assert!(is_vertex_coloring(&g, &c).expect("validate"));
941        }
942
943        let c_ds = vertex_coloring_greedy(&g, GreedyColoringHeuristic::DSatur).expect("ok");
944        let num_colors = c_ds.iter().copied().max().unwrap_or(0) + 1;
945        assert_eq!(num_colors, 3);
946    }
947
948    #[test]
949    fn test_multi_edges_dsatur() {
950        let g = make_undirected(
951            8,
952            &[
953                (0, 3),
954                (0, 4),
955                (1, 4),
956                (2, 4),
957                (1, 5),
958                (2, 5),
959                (3, 5),
960                (0, 6),
961                (1, 6),
962                (2, 6),
963                (3, 6),
964                (0, 7),
965                (1, 7),
966                (2, 7),
967                (4, 7),
968                (5, 7),
969                (0, 4),
970                (0, 4),
971                (3, 5),
972                (3, 5),
973                (3, 5),
974                (0, 0),
975                (0, 0),
976                (1, 1),
977            ],
978        );
979
980        let c = vertex_coloring_greedy(&g, GreedyColoringHeuristic::DSatur).expect("ok");
981        assert!(is_vertex_coloring(&g, &c).expect("validate"));
982        let num_colors = c.iter().copied().max().unwrap_or(0) + 1;
983        assert_eq!(num_colors, 3);
984    }
985
986    #[test]
987    fn test_directed_graph_both() {
988        let g = make_directed(3, &[(0, 1), (1, 2), (2, 0)]);
989        for heuristic in [
990            GreedyColoringHeuristic::ColoredNeighbors,
991            GreedyColoringHeuristic::DSatur,
992        ] {
993            let c = vertex_coloring_greedy(&g, heuristic).expect("ok");
994            assert_eq!(c.len(), 3);
995            for e in 0..g.ecount() {
996                let eid = u32::try_from(e).expect("eid fits u32");
997                let (u, v) = g.edge(eid).expect("edge");
998                assert_ne!(c[u as usize], c[v as usize], "edge ({u},{v}) conflict");
999            }
1000        }
1001    }
1002
1003    // ---- is_vertex_coloring ----
1004
1005    #[test]
1006    fn test_is_vertex_coloring_valid() {
1007        let g = make_undirected(3, &[(0, 1), (1, 2), (2, 0)]);
1008        assert!(is_vertex_coloring(&g, &[0, 1, 2]).expect("ok"));
1009    }
1010
1011    #[test]
1012    fn test_is_vertex_coloring_invalid() {
1013        let g = make_undirected(3, &[(0, 1), (1, 2), (2, 0)]);
1014        assert!(!is_vertex_coloring(&g, &[0, 0, 1]).expect("ok"));
1015    }
1016
1017    #[test]
1018    fn test_is_vertex_coloring_self_loop_ok() {
1019        let g = make_undirected(2, &[(0, 0), (0, 1)]);
1020        assert!(is_vertex_coloring(&g, &[0, 1]).expect("ok"));
1021    }
1022
1023    #[test]
1024    fn test_is_vertex_coloring_wrong_size() {
1025        let g = make_undirected(3, &[(0, 1)]);
1026        assert!(is_vertex_coloring(&g, &[0, 1]).is_err());
1027    }
1028
1029    // ---- is_bipartite_coloring ----
1030
1031    #[test]
1032    fn test_bipartite_coloring_valid_undirected() {
1033        let g = make_undirected(4, &[(0, 2), (0, 3), (1, 2), (1, 3)]);
1034        let r = is_bipartite_coloring(&g, &[false, false, true, true]).expect("ok");
1035        assert!(r.valid);
1036        assert_eq!(r.mode, Some(BipartiteEdgeDirection::All));
1037    }
1038
1039    #[test]
1040    fn test_bipartite_coloring_invalid() {
1041        let g = make_undirected(4, &[(0, 1), (0, 2), (1, 3), (2, 3)]);
1042        let r = is_bipartite_coloring(&g, &[false, false, true, true]).expect("ok");
1043        assert!(!r.valid);
1044    }
1045
1046    #[test]
1047    fn test_bipartite_coloring_directed_out() {
1048        let g = make_directed(4, &[(0, 2), (0, 3), (1, 2), (1, 3)]);
1049        let r = is_bipartite_coloring(&g, &[false, false, true, true]).expect("ok");
1050        assert!(r.valid);
1051        assert_eq!(r.mode, Some(BipartiteEdgeDirection::Out));
1052    }
1053
1054    #[test]
1055    fn test_bipartite_coloring_directed_in() {
1056        let g = make_directed(4, &[(2, 0), (3, 0), (2, 1), (3, 1)]);
1057        let r = is_bipartite_coloring(&g, &[false, false, true, true]).expect("ok");
1058        assert!(r.valid);
1059        assert_eq!(r.mode, Some(BipartiteEdgeDirection::In));
1060    }
1061
1062    #[test]
1063    fn test_bipartite_coloring_directed_all() {
1064        let g = make_directed(4, &[(0, 2), (2, 1), (1, 3), (3, 0)]);
1065        let r = is_bipartite_coloring(&g, &[false, false, true, true]).expect("ok");
1066        assert!(r.valid);
1067        assert_eq!(r.mode, Some(BipartiteEdgeDirection::All));
1068    }
1069
1070    #[test]
1071    fn test_bipartite_coloring_wrong_size() {
1072        let g = make_undirected(3, &[(0, 1)]);
1073        assert!(is_bipartite_coloring(&g, &[false, true]).is_err());
1074    }
1075
1076    // ---- is_edge_coloring ----
1077
1078    #[test]
1079    fn test_edge_coloring_valid_triangle() {
1080        let g = make_undirected(3, &[(0, 1), (1, 2), (2, 0)]);
1081        assert!(is_edge_coloring(&g, &[0, 1, 2]).expect("ok"));
1082    }
1083
1084    #[test]
1085    fn test_edge_coloring_invalid_triangle() {
1086        let g = make_undirected(3, &[(0, 1), (1, 2), (2, 0)]);
1087        assert!(!is_edge_coloring(&g, &[0, 1, 0]).expect("ok"));
1088    }
1089
1090    #[test]
1091    fn test_edge_coloring_path() {
1092        let g = make_undirected(4, &[(0, 1), (1, 2), (2, 3)]);
1093        assert!(is_edge_coloring(&g, &[0, 1, 0]).expect("ok"));
1094    }
1095
1096    #[test]
1097    fn test_edge_coloring_wrong_size() {
1098        let g = make_undirected(2, &[(0, 1)]);
1099        assert!(is_edge_coloring(&g, &[0, 1]).is_err());
1100    }
1101
1102    #[test]
1103    fn test_edge_coloring_empty() {
1104        let g = make_undirected(0, &[]);
1105        assert!(is_edge_coloring(&g, &[]).expect("ok"));
1106    }
1107
1108    // ---- edge_coloring_greedy tests ----
1109
1110    #[test]
1111    fn test_edge_coloring_greedy_empty() {
1112        let g = make_undirected(0, &[]);
1113        let colors = edge_coloring_greedy(&g).unwrap();
1114        assert!(colors.is_empty());
1115    }
1116
1117    #[test]
1118    fn test_edge_coloring_greedy_no_edges() {
1119        let g = make_undirected(5, &[]);
1120        let colors = edge_coloring_greedy(&g).unwrap();
1121        assert!(colors.is_empty());
1122    }
1123
1124    #[test]
1125    fn test_edge_coloring_greedy_single_edge() {
1126        let g = make_undirected(2, &[(0, 1)]);
1127        let colors = edge_coloring_greedy(&g).unwrap();
1128        assert_eq!(colors.len(), 1);
1129        assert_eq!(colors[0], 0);
1130        assert!(is_edge_coloring(&g, &colors).unwrap());
1131    }
1132
1133    #[test]
1134    fn test_edge_coloring_greedy_path() {
1135        let g = make_undirected(4, &[(0, 1), (1, 2), (2, 3)]);
1136        let colors = edge_coloring_greedy(&g).unwrap();
1137        assert!(is_edge_coloring(&g, &colors).unwrap());
1138        assert_eq!(edge_chromatic_number(&colors), 2); // path needs 2
1139    }
1140
1141    #[test]
1142    fn test_edge_coloring_greedy_triangle() {
1143        let g = make_undirected(3, &[(0, 1), (1, 2), (2, 0)]);
1144        let colors = edge_coloring_greedy(&g).unwrap();
1145        assert!(is_edge_coloring(&g, &colors).unwrap());
1146        assert_eq!(edge_chromatic_number(&colors), 3); // odd cycle needs 3
1147    }
1148
1149    #[test]
1150    fn test_edge_coloring_greedy_k4() {
1151        let g = make_undirected(4, &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]);
1152        let colors = edge_coloring_greedy(&g).unwrap();
1153        assert!(is_edge_coloring(&g, &colors).unwrap());
1154        // K4 max degree = 3, Vizing: needs 3 or 4
1155        assert!(edge_chromatic_number(&colors) >= 3);
1156    }
1157
1158    #[test]
1159    fn test_edge_coloring_greedy_star() {
1160        let g = make_undirected(5, &[(0, 1), (0, 2), (0, 3), (0, 4)]);
1161        let colors = edge_coloring_greedy(&g).unwrap();
1162        assert!(is_edge_coloring(&g, &colors).unwrap());
1163        assert_eq!(edge_chromatic_number(&colors), 4); // star: Δ colors
1164    }
1165
1166    #[test]
1167    fn test_edge_coloring_greedy_bipartite() {
1168        let g = make_undirected(4, &[(0, 2), (0, 3), (1, 2), (1, 3)]);
1169        let colors = edge_coloring_greedy(&g).unwrap();
1170        assert!(is_edge_coloring(&g, &colors).unwrap());
1171    }
1172
1173    #[test]
1174    fn test_edge_coloring_greedy_self_loop() {
1175        let g = make_undirected(3, &[(0, 0), (0, 1), (1, 2)]);
1176        let colors = edge_coloring_greedy(&g).unwrap();
1177        assert!(is_edge_coloring(&g, &colors).unwrap());
1178    }
1179
1180    #[test]
1181    fn test_edge_coloring_greedy_parallel_edges() {
1182        let g = make_undirected(2, &[(0, 1), (0, 1), (0, 1)]);
1183        let colors = edge_coloring_greedy(&g).unwrap();
1184        assert!(is_edge_coloring(&g, &colors).unwrap());
1185        assert_eq!(edge_chromatic_number(&colors), 3);
1186    }
1187
1188    #[test]
1189    fn test_edge_coloring_greedy_directed() {
1190        let g = Graph::new(3, true).unwrap();
1191        let mut g = g;
1192        g.add_edge(0, 1).unwrap();
1193        g.add_edge(1, 2).unwrap();
1194        g.add_edge(2, 0).unwrap();
1195        let colors = edge_coloring_greedy(&g).unwrap();
1196        assert!(is_edge_coloring(&g, &colors).unwrap());
1197    }
1198
1199    #[test]
1200    fn test_edge_chromatic_number_empty() {
1201        assert_eq!(edge_chromatic_number(&[]), 0);
1202    }
1203
1204    #[test]
1205    fn test_edge_chromatic_number_values() {
1206        assert_eq!(edge_chromatic_number(&[0]), 1);
1207        assert_eq!(edge_chromatic_number(&[0, 1, 2]), 3);
1208        assert_eq!(edge_chromatic_number(&[0, 0, 0]), 1);
1209    }
1210
1211    // ---- vertex_chromatic_number + chromatic_number_upper_bound ----
1212
1213    #[test]
1214    fn test_vertex_chromatic_number_empty() {
1215        assert_eq!(vertex_chromatic_number(&[]), 0);
1216    }
1217
1218    #[test]
1219    fn test_vertex_chromatic_number_values() {
1220        assert_eq!(vertex_chromatic_number(&[0]), 1);
1221        assert_eq!(vertex_chromatic_number(&[0, 1, 2]), 3);
1222        assert_eq!(vertex_chromatic_number(&[0, 0, 0]), 1);
1223        assert_eq!(vertex_chromatic_number(&[0, 1, 0, 1]), 2);
1224    }
1225
1226    #[test]
1227    fn test_chromatic_upper_empty() {
1228        let g = make_undirected(0, &[]);
1229        assert_eq!(chromatic_number_upper_bound(&g).unwrap(), 0);
1230    }
1231
1232    #[test]
1233    fn test_chromatic_upper_singleton() {
1234        let g = make_undirected(1, &[]);
1235        assert_eq!(chromatic_number_upper_bound(&g).unwrap(), 1);
1236    }
1237
1238    #[test]
1239    fn test_chromatic_upper_no_edges() {
1240        let g = make_undirected(5, &[]);
1241        assert_eq!(chromatic_number_upper_bound(&g).unwrap(), 1);
1242    }
1243
1244    #[test]
1245    fn test_chromatic_upper_single_edge() {
1246        let g = make_undirected(2, &[(0, 1)]);
1247        assert_eq!(chromatic_number_upper_bound(&g).unwrap(), 2);
1248    }
1249
1250    #[test]
1251    fn test_chromatic_upper_triangle() {
1252        let g = make_undirected(3, &[(0, 1), (1, 2), (2, 0)]);
1253        assert_eq!(chromatic_number_upper_bound(&g).unwrap(), 3);
1254    }
1255
1256    #[test]
1257    fn test_chromatic_upper_bipartite_c4() {
1258        let g = make_undirected(4, &[(0, 1), (1, 2), (2, 3), (3, 0)]);
1259        assert_eq!(chromatic_number_upper_bound(&g).unwrap(), 2);
1260    }
1261
1262    #[test]
1263    fn test_chromatic_upper_k4() {
1264        let g = make_undirected(4, &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]);
1265        assert_eq!(chromatic_number_upper_bound(&g).unwrap(), 4);
1266    }
1267
1268    #[test]
1269    fn test_chromatic_upper_path() {
1270        let g = make_undirected(4, &[(0, 1), (1, 2), (2, 3)]);
1271        assert_eq!(chromatic_number_upper_bound(&g).unwrap(), 2);
1272    }
1273
1274    #[test]
1275    fn test_chromatic_upper_star() {
1276        let g = make_undirected(5, &[(0, 1), (0, 2), (0, 3), (0, 4)]);
1277        assert_eq!(chromatic_number_upper_bound(&g).unwrap(), 2);
1278    }
1279
1280    #[test]
1281    fn test_chromatic_upper_self_loop() {
1282        let g = make_undirected(2, &[(0, 0), (0, 1)]);
1283        assert_eq!(chromatic_number_upper_bound(&g).unwrap(), 2);
1284    }
1285
1286    #[test]
1287    fn test_chromatic_upper_directed() {
1288        let g = make_directed(3, &[(0, 1), (1, 2), (2, 0)]);
1289        assert_eq!(chromatic_number_upper_bound(&g).unwrap(), 3);
1290    }
1291
1292    #[test]
1293    fn test_chromatic_upper_petersen() {
1294        let g = make_undirected(
1295            10,
1296            &[
1297                (0, 1),
1298                (1, 2),
1299                (2, 3),
1300                (3, 4),
1301                (4, 0),
1302                (5, 7),
1303                (7, 9),
1304                (9, 6),
1305                (6, 8),
1306                (8, 5),
1307                (0, 5),
1308                (1, 6),
1309                (2, 7),
1310                (3, 8),
1311                (4, 9),
1312            ],
1313        );
1314        let chi = chromatic_number_upper_bound(&g).unwrap();
1315        assert!(chi >= 3);
1316        assert!(chi <= 4);
1317    }
1318}
1319
1320// =================================================================
1321// Proptest
1322// =================================================================
1323
1324#[cfg(test)]
1325#[cfg(all(test, feature = "proptest-harness"))]
1326mod proptests {
1327    use super::*;
1328    use crate::create;
1329    use proptest::prelude::*;
1330
1331    proptest! {
1332        #[test]
1333        fn prop_cn_valid_coloring(n in 2u32..50, edge_count in 1usize..100) {
1334            let max_edges = (n as usize) * (n as usize - 1) / 2;
1335            let actual_edges = edge_count.min(max_edges);
1336            let mut edges = Vec::with_capacity(actual_edges);
1337            let mut rng_state = 0x1234_5678u64;
1338            for _ in 0..actual_edges {
1339                rng_state = rng_state.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
1340                let u = (rng_state >> 32) as u32 % n;
1341                rng_state = rng_state.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
1342                let v = (rng_state >> 32) as u32 % n;
1343                if u != v {
1344                    edges.push((u, v));
1345                }
1346            }
1347            let g = create(&edges, n, false).expect("graph");
1348            let c = vertex_coloring_greedy(&g, GreedyColoringHeuristic::ColoredNeighbors)
1349                .expect("cn");
1350            prop_assert!(is_vertex_coloring(&g, &c).expect("validate"));
1351        }
1352
1353        #[test]
1354        fn prop_dsatur_valid_coloring(n in 2u32..50, edge_count in 1usize..100) {
1355            let max_edges = (n as usize) * (n as usize - 1) / 2;
1356            let actual_edges = edge_count.min(max_edges);
1357            let mut edges = Vec::with_capacity(actual_edges);
1358            let mut rng_state = 0xABCD_EF01u64;
1359            for _ in 0..actual_edges {
1360                rng_state = rng_state.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
1361                let u = (rng_state >> 32) as u32 % n;
1362                rng_state = rng_state.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
1363                let v = (rng_state >> 32) as u32 % n;
1364                if u != v {
1365                    edges.push((u, v));
1366                }
1367            }
1368            let g = create(&edges, n, false).expect("graph");
1369            let c = vertex_coloring_greedy(&g, GreedyColoringHeuristic::DSatur)
1370                .expect("dsatur");
1371            prop_assert!(is_vertex_coloring(&g, &c).expect("validate"));
1372        }
1373
1374        #[test]
1375        fn prop_dsatur_leq_cn_colors(n in 3u32..30, edge_count in 3usize..60) {
1376            let max_edges = (n as usize) * (n as usize - 1) / 2;
1377            let actual_edges = edge_count.min(max_edges);
1378            let mut edges = Vec::with_capacity(actual_edges);
1379            let mut rng_state = 0xDEAD_BEEFu64;
1380            for _ in 0..actual_edges {
1381                rng_state = rng_state.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
1382                let u = (rng_state >> 32) as u32 % n;
1383                rng_state = rng_state.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
1384                let v = (rng_state >> 32) as u32 % n;
1385                if u != v {
1386                    edges.push((u, v));
1387                }
1388            }
1389            let g = create(&edges, n, false).expect("graph");
1390            let c_cn = vertex_coloring_greedy(&g, GreedyColoringHeuristic::ColoredNeighbors)
1391                .expect("cn");
1392            let c_ds = vertex_coloring_greedy(&g, GreedyColoringHeuristic::DSatur)
1393                .expect("dsatur");
1394            let n_cn = c_cn.iter().copied().max().unwrap_or(0) + 1;
1395            let n_ds = c_ds.iter().copied().max().unwrap_or(0) + 1;
1396            prop_assert!(is_vertex_coloring(&g, &c_cn).expect("validate cn"));
1397            prop_assert!(is_vertex_coloring(&g, &c_ds).expect("validate ds"));
1398            prop_assert!(n_ds <= n_cn + 2, "DSATUR used {n_ds} colors vs CN {n_cn}");
1399        }
1400
1401        #[test]
1402        fn prop_coloring_uses_contiguous_colors(n in 1u32..40, edge_count in 0usize..80) {
1403            let mut edges = Vec::new();
1404            let mut rng_state = 0xCAFE_BABEu64;
1405            for _ in 0..edge_count {
1406                rng_state = rng_state.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
1407                let u = (rng_state >> 32) as u32 % n;
1408                rng_state = rng_state.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
1409                let v = (rng_state >> 32) as u32 % n;
1410                if u != v {
1411                    edges.push((u, v));
1412                }
1413            }
1414            let g = create(&edges, n, false).expect("graph");
1415            let c = vertex_coloring_greedy(&g, GreedyColoringHeuristic::DSatur).expect("dsatur");
1416            if !c.is_empty() {
1417                let max_color = c.iter().copied().max().unwrap_or(0);
1418                let mut used = vec![false; (max_color + 1) as usize];
1419                for &col in &c {
1420                    used[col as usize] = true;
1421                }
1422                for (i, &u) in used.iter().enumerate() {
1423                    prop_assert!(u, "color {i} not used but max is {max_color}");
1424                }
1425            }
1426        }
1427
1428        #[test]
1429        fn prop_bipartite_coloring_after_dsatur(n1 in 1u32..15, n2 in 1u32..15) {
1430            let n = n1 + n2;
1431            let mut edges = Vec::new();
1432            for u in 0..n1 {
1433                for v in n1..n {
1434                    edges.push((u, v));
1435                }
1436            }
1437            let g = create(&edges, n, false).expect("bipartite");
1438            let c = vertex_coloring_greedy(&g, GreedyColoringHeuristic::DSatur).expect("dsatur");
1439            let num_colors = c.iter().copied().max().unwrap_or(0) + 1;
1440            prop_assert_eq!(num_colors, 2, "bipartite graph should need exactly 2 colors");
1441        }
1442    }
1443}