1069-graph-coloring — Graph Coloring
Tutorial
The Problem
Graph coloring assigns colors to vertices such that no two adjacent vertices share a color, using at most k colors. It models register allocation in compilers (colors = registers, vertices = variables, edges = interference), exam scheduling (colors = time slots, vertices = exams, edges = shared students), and map coloring.
Graph coloring is NP-complete in general but tractable for small k or special graph families. The backtracking approach tries each color and backtracks when a conflict is found.
🎯 Learning Outcomes
is_safe predicate that checks all neighborsCode Example
#![allow(clippy::all)]
// 1069: Graph Coloring — Backtracking
// Approach 1: Adjacency matrix
fn graph_coloring(adj: &[Vec<i32>], num_colors: usize) -> Option<Vec<usize>> {
let n = adj.len();
let mut colors = vec![0usize; n];
fn is_safe(node: usize, color: usize, adj: &[Vec<i32>], colors: &[usize]) -> bool {
for i in 0..adj.len() {
if adj[node][i] == 1 && colors[i] == color {
return false;
}
}
true
}
fn solve(node: usize, adj: &[Vec<i32>], colors: &mut Vec<usize>, num_colors: usize) -> bool {
if node == adj.len() {
return true;
}
for c in 1..=num_colors {
if is_safe(node, c, adj, colors) {
colors[node] = c;
if solve(node + 1, adj, colors, num_colors) {
return true;
}
colors[node] = 0;
}
}
false
}
if solve(0, adj, &mut colors, num_colors) {
Some(colors)
} else {
None
}
}
// Approach 2: Adjacency list
fn graph_coloring_list(adj: &[Vec<usize>], num_colors: usize) -> Option<Vec<usize>> {
let n = adj.len();
let mut colors = vec![0usize; n];
fn is_safe(node: usize, color: usize, adj: &[Vec<usize>], colors: &[usize]) -> bool {
adj[node].iter().all(|&neighbor| colors[neighbor] != color)
}
fn solve(node: usize, adj: &[Vec<usize>], colors: &mut Vec<usize>, num_colors: usize) -> bool {
if node == adj.len() {
return true;
}
for c in 1..=num_colors {
if is_safe(node, c, adj, colors) {
colors[node] = c;
if solve(node + 1, adj, colors, num_colors) {
return true;
}
colors[node] = 0;
}
}
false
}
if solve(0, adj, &mut colors, num_colors) {
Some(colors)
} else {
None
}
}
// Approach 3: Greedy coloring (not optimal but fast)
fn greedy_coloring(adj: &[Vec<usize>]) -> Vec<usize> {
let n = adj.len();
let mut colors = vec![0usize; n];
for node in 0..n {
let used: std::collections::HashSet<usize> = adj[node]
.iter()
.filter(|&&nb| colors[nb] > 0)
.map(|&nb| colors[nb])
.collect();
colors[node] = (1..).find(|c| !used.contains(c)).unwrap();
}
colors
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_graph_coloring_possible() {
let adj = vec![
vec![0, 1, 1, 1],
vec![1, 0, 1, 0],
vec![1, 1, 0, 1],
vec![1, 0, 1, 0],
];
let colors = graph_coloring(&adj, 3).unwrap();
assert_eq!(colors.len(), 4);
// Verify no adjacent nodes share color
for i in 0..4 {
for j in 0..4 {
if adj[i][j] == 1 {
assert_ne!(colors[i], colors[j]);
}
}
}
}
#[test]
fn test_graph_coloring_impossible() {
let adj = vec![
vec![0, 1, 1, 1],
vec![1, 0, 1, 0],
vec![1, 1, 0, 1],
vec![1, 0, 1, 0],
];
assert!(graph_coloring(&adj, 2).is_none());
}
#[test]
fn test_graph_coloring_list() {
let adj = vec![vec![1, 2, 3], vec![0, 2], vec![0, 1, 3], vec![0, 2]];
let colors = graph_coloring_list(&adj, 3).unwrap();
assert_eq!(colors.len(), 4);
}
#[test]
fn test_greedy() {
let adj = vec![vec![1, 2, 3], vec![0, 2], vec![0, 1, 3], vec![0, 2]];
let colors = greedy_coloring(&adj);
for (node, neighbors) in adj.iter().enumerate() {
for &nb in neighbors {
assert_ne!(colors[node], colors[nb]);
}
}
}
}Key Differences
is_safe complexity**: Rust's is_safe iterates all vertices and checks adj[node][i] == 1; OCaml's Array.for_all2 is equivalent.for c in 1..=num_colors loop; OCaml's tail-recursive try_colors is equivalent.Option<Vec<usize>> / option (int list) — None when coloring is impossible.OCaml Approach
let graph_color adj k =
let n = Array.length adj in
let colors = Array.make n 0 in
let is_safe node color =
Array.for_all2 (fun neighbor c -> adj.(node).(neighbor) = 0 || c <> color) adj.(node) colors
in
let rec solve node =
if node = n then true
else
let rec try_colors c =
if c > k then false
else if is_safe node c then begin
colors.(node) <- c;
if solve (node + 1) then true
else begin colors.(node) <- 0; try_colors (c + 1) end
end else try_colors (c + 1)
in
try_colors 1
in
if solve 0 then Some (Array.to_list colors) else None
Structurally identical. Both implement the same backtracking search.
Full Source
#![allow(clippy::all)]
// 1069: Graph Coloring — Backtracking
// Approach 1: Adjacency matrix
fn graph_coloring(adj: &[Vec<i32>], num_colors: usize) -> Option<Vec<usize>> {
let n = adj.len();
let mut colors = vec![0usize; n];
fn is_safe(node: usize, color: usize, adj: &[Vec<i32>], colors: &[usize]) -> bool {
for i in 0..adj.len() {
if adj[node][i] == 1 && colors[i] == color {
return false;
}
}
true
}
fn solve(node: usize, adj: &[Vec<i32>], colors: &mut Vec<usize>, num_colors: usize) -> bool {
if node == adj.len() {
return true;
}
for c in 1..=num_colors {
if is_safe(node, c, adj, colors) {
colors[node] = c;
if solve(node + 1, adj, colors, num_colors) {
return true;
}
colors[node] = 0;
}
}
false
}
if solve(0, adj, &mut colors, num_colors) {
Some(colors)
} else {
None
}
}
// Approach 2: Adjacency list
fn graph_coloring_list(adj: &[Vec<usize>], num_colors: usize) -> Option<Vec<usize>> {
let n = adj.len();
let mut colors = vec![0usize; n];
fn is_safe(node: usize, color: usize, adj: &[Vec<usize>], colors: &[usize]) -> bool {
adj[node].iter().all(|&neighbor| colors[neighbor] != color)
}
fn solve(node: usize, adj: &[Vec<usize>], colors: &mut Vec<usize>, num_colors: usize) -> bool {
if node == adj.len() {
return true;
}
for c in 1..=num_colors {
if is_safe(node, c, adj, colors) {
colors[node] = c;
if solve(node + 1, adj, colors, num_colors) {
return true;
}
colors[node] = 0;
}
}
false
}
if solve(0, adj, &mut colors, num_colors) {
Some(colors)
} else {
None
}
}
// Approach 3: Greedy coloring (not optimal but fast)
fn greedy_coloring(adj: &[Vec<usize>]) -> Vec<usize> {
let n = adj.len();
let mut colors = vec![0usize; n];
for node in 0..n {
let used: std::collections::HashSet<usize> = adj[node]
.iter()
.filter(|&&nb| colors[nb] > 0)
.map(|&nb| colors[nb])
.collect();
colors[node] = (1..).find(|c| !used.contains(c)).unwrap();
}
colors
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_graph_coloring_possible() {
let adj = vec![
vec![0, 1, 1, 1],
vec![1, 0, 1, 0],
vec![1, 1, 0, 1],
vec![1, 0, 1, 0],
];
let colors = graph_coloring(&adj, 3).unwrap();
assert_eq!(colors.len(), 4);
// Verify no adjacent nodes share color
for i in 0..4 {
for j in 0..4 {
if adj[i][j] == 1 {
assert_ne!(colors[i], colors[j]);
}
}
}
}
#[test]
fn test_graph_coloring_impossible() {
let adj = vec![
vec![0, 1, 1, 1],
vec![1, 0, 1, 0],
vec![1, 1, 0, 1],
vec![1, 0, 1, 0],
];
assert!(graph_coloring(&adj, 2).is_none());
}
#[test]
fn test_graph_coloring_list() {
let adj = vec![vec![1, 2, 3], vec![0, 2], vec![0, 1, 3], vec![0, 2]];
let colors = graph_coloring_list(&adj, 3).unwrap();
assert_eq!(colors.len(), 4);
}
#[test]
fn test_greedy() {
let adj = vec![vec![1, 2, 3], vec![0, 2], vec![0, 1, 3], vec![0, 2]];
let colors = greedy_coloring(&adj);
for (node, neighbors) in adj.iter().enumerate() {
for &nb in neighbors {
assert_ne!(colors[node], colors[nb]);
}
}
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_graph_coloring_possible() {
let adj = vec![
vec![0, 1, 1, 1],
vec![1, 0, 1, 0],
vec![1, 1, 0, 1],
vec![1, 0, 1, 0],
];
let colors = graph_coloring(&adj, 3).unwrap();
assert_eq!(colors.len(), 4);
// Verify no adjacent nodes share color
for i in 0..4 {
for j in 0..4 {
if adj[i][j] == 1 {
assert_ne!(colors[i], colors[j]);
}
}
}
}
#[test]
fn test_graph_coloring_impossible() {
let adj = vec![
vec![0, 1, 1, 1],
vec![1, 0, 1, 0],
vec![1, 1, 0, 1],
vec![1, 0, 1, 0],
];
assert!(graph_coloring(&adj, 2).is_none());
}
#[test]
fn test_graph_coloring_list() {
let adj = vec![vec![1, 2, 3], vec![0, 2], vec![0, 1, 3], vec![0, 2]];
let colors = graph_coloring_list(&adj, 3).unwrap();
assert_eq!(colors.len(), 4);
}
#[test]
fn test_greedy() {
let adj = vec![vec![1, 2, 3], vec![0, 2], vec![0, 1, 3], vec![0, 2]];
let colors = greedy_coloring(&adj);
for (node, neighbors) in adj.iter().enumerate() {
for &nb in neighbors {
assert_ne!(colors[node], colors[nb]);
}
}
}
}
Deep Comparison
Graph Coloring — Comparison
Core Insight
Graph coloring assigns colors to vertices so no edge connects same-colored vertices. Backtracking tries each color, backtracks on conflict. Greedy coloring is fast but may use more colors than optimal.
OCaml Approach
Array for colors with 0 as uncolored sentinelList.for_all for adjacency list safety checkref flag for early exit from color loopRust Approach
Vec<usize> for colors, inner fn for recursion.iter().all() for adjacency list safety — idiomaticHashSet in greedy approach to find first unused color(1..).find() — infinite iterator to find smallest available colorComparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Safety check (list) | List.for_all | .iter().all() |
| Early exit | ref flag + not !found | return true/false |
| Greedy first-fit | Would use List.find | (1..).find(\|c\| !used.contains(c)) |
| Graph representation | int array array or int list array | Vec<Vec<i32>> or Vec<Vec<usize>> |
Exercises
chromatic_number(adj: &[Vec<i32>]) -> usize that finds the minimum number of colors needed by trying k = 1, 2, ... until a valid coloring exists.is_bipartite(adj: &[Vec<i32>]) -> bool function using BFS 2-coloring as a special case.