Overview
Graph coloring assigns labels (colors) to graph elements subject to constraints that adjacent elements receive different colors. The vertex coloring problem assigns colors to vertices such that no two adjacent vertices share the same color. The chromatic number represents the minimum number of colors needed to color a graph following these constraints.
The problem originates from the Four Color Theorem, which states that any planar map can be colored with four colors such that no adjacent regions share the same color. Graph coloring extends beyond cartography into scheduling, register allocation in compilers, frequency assignment in wireless networks, and pattern matching.
# Simple graph representation showing adjacency
graph = {
'A' => ['B', 'C'],
'B' => ['A', 'C', 'D'],
'C' => ['A', 'B', 'D'],
'D' => ['B', 'C']
}
# A valid 3-coloring of this graph
coloring = {
'A' => 'red',
'B' => 'blue',
'C' => 'green',
'D' => 'red'
}
The vertex coloring problem belongs to the class of NP-complete problems, meaning no known polynomial-time algorithm exists for finding optimal colorings of arbitrary graphs. However, specific graph types (bipartite graphs, trees, interval graphs) admit efficient coloring algorithms. Greedy approaches produce approximate solutions with bounded performance guarantees.
Key Principles
Graph coloring operates on undirected graphs G = (V, E) where V represents vertices and E represents edges connecting vertex pairs. A k-coloring assigns one of k colors to each vertex such that vertices connected by an edge receive distinct colors. The chromatic number χ(G) denotes the smallest k for which a valid k-coloring exists.
Several graph properties determine coloring complexity. The maximum degree Δ(G) provides an upper bound: any graph can be colored with Δ(G) + 1 colors using a greedy algorithm. Bipartite graphs have chromatic number 2, while complete graphs on n vertices require n colors. Planar graphs require at most four colors, though determining the chromatic number for a specific planar graph remains NP-complete.
# Graph with known chromatic properties
class Graph
attr_reader :vertices, :edges
def initialize
@vertices = []
@edges = {}
end
def add_vertex(vertex)
@vertices << vertex
@edges[vertex] = []
end
def add_edge(v1, v2)
@edges[v1] << v2
@edges[v2] << v1
end
def degree(vertex)
@edges[vertex].length
end
def max_degree
@vertices.map { |v| degree(v) }.max
end
end
Proper coloring ensures that the coloring function maps each vertex to a color while satisfying constraints. An improper coloring violates the adjacency constraint, creating conflicts where adjacent vertices share colors. The coloring problem decomposes into finding independent sets: vertices receiving the same color form an independent set with no internal edges.
Graph coloring relates to other graph problems through reductions. The clique problem connects to coloring through complement graphs: a k-clique in G becomes an independent set requiring k colors in the complement. The satisfiability problem reduces to graph coloring, establishing NP-completeness. Conversely, coloring reduces to SAT, enabling SAT solver application to coloring problems.
# Checking coloring validity
def valid_coloring?(graph, coloring)
graph.vertices.all? do |vertex|
neighbors = graph.edges[vertex]
neighbors.none? { |neighbor| coloring[vertex] == coloring[neighbor] }
end
end
# Example validation
graph = Graph.new
['A', 'B', 'C'].each { |v| graph.add_vertex(v) }
graph.add_edge('A', 'B')
graph.add_edge('B', 'C')
coloring = { 'A' => 1, 'B' => 2, 'C' => 1 }
valid_coloring?(graph, coloring) # => true
The greedy coloring strategy processes vertices in sequence, assigning each vertex the smallest available color not used by its neighbors. The vertex ordering significantly impacts the number of colors used. Worst-case vertex orderings force greedy algorithms to use Δ(G) + 1 colors, while optimal orderings may achieve χ(G) colors.
Ruby Implementation
Ruby graph coloring implementations typically represent graphs as adjacency lists using hashes. This representation provides efficient neighbor lookups during color assignment and constraint checking.
class GraphColoring
attr_reader :graph, :coloring, :num_colors
def initialize(graph)
@graph = graph
@coloring = {}
@num_colors = 0
end
def greedy_color(vertex_order = nil)
order = vertex_order || @graph.vertices
order.each do |vertex|
used_colors = @graph.edges[vertex].map { |neighbor| @coloring[neighbor] }.compact.to_set
color = (1..Float::INFINITY).lazy.find { |c| !used_colors.include?(c) }
@coloring[vertex] = color
@num_colors = [@num_colors, color].max
end
@coloring
end
def color_count
@num_colors
end
end
# Usage example
graph = Graph.new
vertices = ['A', 'B', 'C', 'D', 'E']
vertices.each { |v| graph.add_vertex(v) }
graph.add_edge('A', 'B')
graph.add_edge('A', 'C')
graph.add_edge('B', 'D')
graph.add_edge('C', 'D')
graph.add_edge('D', 'E')
coloring = GraphColoring.new(graph)
coloring.greedy_color
# => {"A"=>1, "B"=>2, "C"=>2, "D"=>1, "E"=>2}
Welsh-Powell ordering improves greedy coloring by processing vertices in descending degree order. Vertices with higher degrees receive colors first, reducing the total color count for many graph types.
class GraphColoring
def welsh_powell_order
@graph.vertices.sort_by { |v| -@graph.degree(v) }
end
def welsh_powell_coloring
order = welsh_powell_order
greedy_color(order)
end
end
# Comparison of orderings
coloring1 = GraphColoring.new(graph)
coloring1.greedy_color # Arbitrary order
coloring2 = GraphColoring.new(graph)
coloring2.welsh_powell_coloring # Degree-based order
puts "Arbitrary ordering: #{coloring1.color_count} colors"
puts "Welsh-Powell ordering: #{coloring2.color_count} colors"
Backtracking algorithms explore the solution space systematically, finding optimal colorings at the cost of exponential worst-case complexity. The algorithm tries assigning each color to a vertex, recursively coloring remaining vertices, and backtracking when conflicts occur.
class BacktrackingColoring
def initialize(graph, max_colors)
@graph = graph
@max_colors = max_colors
@coloring = {}
@solution = nil
end
def solve
return @solution if @solution
if backtrack(@graph.vertices, 0)
@solution = @coloring.dup
end
@solution
end
private
def backtrack(vertices, index)
return true if index == vertices.length
vertex = vertices[index]
(1..@max_colors).each do |color|
if safe_color?(vertex, color)
@coloring[vertex] = color
return true if backtrack(vertices, index + 1)
@coloring.delete(vertex)
end
end
false
end
def safe_color?(vertex, color)
@graph.edges[vertex].none? { |neighbor| @coloring[neighbor] == color }
end
end
# Finding minimum colors
def find_chromatic_number(graph)
(1..graph.vertices.length).each do |k|
solver = BacktrackingColoring.new(graph, k)
return k if solver.solve
end
end
Ruby's flexibility enables domain-specific coloring implementations. Register allocation in compilers maps variables to registers, where interference graphs represent conflicting live ranges. Scheduling problems model incompatible activities as adjacent vertices.
# Register allocation example
class RegisterAllocator
def initialize(interference_graph, num_registers)
@graph = interference_graph
@num_registers = num_registers
end
def allocate
coloring = GraphColoring.new(@graph)
coloring.welsh_powell_coloring
if coloring.color_count <= @num_registers
map_colors_to_registers(coloring.coloring)
else
spill_variables(coloring)
end
end
private
def map_colors_to_registers(coloring)
register_names = ('r0'..'r31').to_a
coloring.transform_values { |color| register_names[color - 1] }
end
def spill_variables(coloring)
# Variables requiring more colors than available registers
# must be spilled to memory
threshold = @num_registers
spilled = coloring.coloring.select { |_, color| color > threshold }.keys
{ spilled: spilled, allocated: coloring.coloring.reject { |k, _| spilled.include?(k) } }
end
end
Implementation Approaches
Multiple algorithmic strategies address graph coloring with different trade-offs between solution quality and computational cost. The approach selection depends on graph characteristics, required solution optimality, and available computation time.
Greedy sequential coloring processes vertices one at a time, assigning the minimum available color. The algorithm requires O(V + E) time but produces solutions potentially far from optimal. The performance guarantee states that greedy coloring uses at most Δ(G) + 1 colors, where Δ(G) represents the maximum vertex degree.
# Basic greedy implementation with ordering parameter
def greedy_coloring(graph, ordering_strategy)
vertices = case ordering_strategy
when :natural then graph.vertices
when :degree then graph.vertices.sort_by { |v| -graph.degree(v) }
when :random then graph.vertices.shuffle
end
coloring = {}
vertices.each do |vertex|
neighbor_colors = graph.edges[vertex].filter_map { |n| coloring[n] }.to_set
color = (1..).find { |c| !neighbor_colors.include?(c) }
coloring[vertex] = color
end
coloring
end
DSATUR (Degree of Saturation) represents a dynamic ordering strategy that selects vertices based on the number of distinct colors used by their neighbors. Vertices with higher saturation degrees receive colors first, often producing better results than static orderings.
class DSaturColoring
def initialize(graph)
@graph = graph
@coloring = {}
@saturation = Hash.new(0)
end
def color
uncolored = @graph.vertices.to_set
until uncolored.empty?
vertex = select_vertex(uncolored)
assign_color(vertex)
uncolored.delete(vertex)
update_saturation(vertex)
end
@coloring
end
private
def select_vertex(uncolored)
uncolored.max_by do |v|
[@saturation[v], @graph.degree(v), -@graph.vertices.index(v)]
end
end
def assign_color(vertex)
used_colors = @graph.edges[vertex].filter_map { |n| @coloring[n] }.to_set
@coloring[vertex] = (1..).find { |c| !used_colors.include?(c) }
end
def update_saturation(vertex)
@graph.edges[vertex].each do |neighbor|
next if @coloring.key?(neighbor)
neighbor_colors = @graph.edges[neighbor].filter_map { |n| @coloring[n] }
@saturation[neighbor] = neighbor_colors.uniq.length
end
end
end
Backtracking exhaustively searches the solution space, guaranteeing optimal solutions for small graphs. The algorithm explores color assignments recursively, pruning branches when conflicts occur. Pruning strategies significantly impact performance: forward checking eliminates values violating constraints before assignment.
Branch-and-bound extends backtracking with bounds on the objective function. The algorithm maintains the best solution found so far and prunes branches that cannot improve upon it. For coloring, the bound represents a lower estimate of colors needed to complete the current partial coloring.
class BranchAndBoundColoring
def initialize(graph)
@graph = graph
@best_coloring = nil
@best_colors = Float::INFINITY
end
def solve
initial_bound = greedy_upper_bound
branch(@graph.vertices, {}, 0, initial_bound)
@best_coloring
end
private
def branch(vertices, partial_coloring, index, upper_bound)
if index == vertices.length
color_count = partial_coloring.values.max
if color_count < @best_colors
@best_colors = color_count
@best_coloring = partial_coloring.dup
end
return
end
vertex = vertices[index]
lower_bound = compute_lower_bound(partial_coloring, vertices[index..])
return if lower_bound >= @best_colors
(1..upper_bound).each do |color|
if color_safe?(vertex, color, partial_coloring)
partial_coloring[vertex] = color
branch(vertices, partial_coloring, index + 1, upper_bound)
partial_coloring.delete(vertex)
end
end
end
def compute_lower_bound(partial_coloring, remaining_vertices)
used_colors = partial_coloring.values.max || 0
max_clique_size = estimate_max_clique(remaining_vertices)
[used_colors, max_clique_size].max
end
def estimate_max_clique(vertices)
# Simple greedy clique estimation
return 0 if vertices.empty?
clique = [vertices.first]
vertices[1..].each do |v|
if clique.all? { |c| @graph.edges[v].include?(c) }
clique << v
end
end
clique.length
end
def greedy_upper_bound
GraphColoring.new(@graph).welsh_powell_coloring.color_count
end
def color_safe?(vertex, color, coloring)
@graph.edges[vertex].none? { |neighbor| coloring[neighbor] == color }
end
end
Integer programming formulations encode coloring as constraint satisfaction problems solvable by generic solvers. Variables represent vertex-color assignments, with constraints ensuring adjacent vertices receive different colors. Modern solvers handle moderate-sized instances effectively.
Hybrid approaches combine heuristics with exact methods. Iterative greedy algorithms repeatedly apply randomized greedy coloring, retaining the best solution. Simulated annealing and genetic algorithms explore the solution space probabilistically, escaping local optima.
Practical Examples
Graph coloring solves scheduling problems where activities cannot overlap. The exam scheduling scenario assigns time slots to exams such that students with multiple exams receive adequate separation.
class ExamScheduler
def initialize
@exams = []
@conflicts = Hash.new { |h, k| h[k] = [] }
end
def add_exam(exam)
@exams << exam
end
def add_conflict(exam1, exam2)
@conflicts[exam1] << exam2
@conflicts[exam2] << exam1
end
def schedule(time_slots)
graph = build_conflict_graph
coloring = GraphColoring.new(graph)
result = coloring.welsh_powell_coloring
if coloring.color_count <= time_slots.length
map_to_schedule(result, time_slots)
else
{ error: "Need #{coloring.color_count} time slots, only #{time_slots.length} available" }
end
end
private
def build_conflict_graph
graph = Graph.new
@exams.each { |exam| graph.add_vertex(exam) }
@conflicts.each do |exam, conflicts|
conflicts.each { |conflict| graph.add_edge(exam, conflict) }
end
graph
end
def map_to_schedule(coloring, time_slots)
coloring.transform_values { |color| time_slots[color - 1] }
end
end
# Usage example
scheduler = ExamScheduler.new
%w[Math Physics Chemistry Biology English History].each { |exam| scheduler.add_exam(exam) }
# Students taking multiple courses create conflicts
scheduler.add_conflict('Math', 'Physics')
scheduler.add_conflict('Math', 'Chemistry')
scheduler.add_conflict('Physics', 'Biology')
scheduler.add_conflict('Chemistry', 'Biology')
scheduler.add_conflict('English', 'History')
time_slots = ['Monday 9am', 'Monday 2pm', 'Tuesday 9am']
schedule = scheduler.schedule(time_slots)
# => {"Math"=>"Monday 9am", "Physics"=>"Monday 2pm", "Chemistry"=>"Tuesday 9am", ...}
Map coloring demonstrates classical graph coloring applications. Regions sharing borders cannot receive identical colors, requiring minimum colors to distinguish all regions.
class MapColoring
def initialize
@regions = {}
end
def add_region(name, borders)
@regions[name] = borders
end
def color_map
graph = build_region_graph
coloring = DSaturColoring.new(graph)
result = coloring.color
color_names = %w[Red Blue Green Yellow Orange Purple]
result.transform_values { |num| color_names[num - 1] }
end
private
def build_region_graph
graph = Graph.new
@regions.keys.each { |region| graph.add_vertex(region) }
@regions.each do |region, borders|
borders.each { |border| graph.add_edge(region, border) if @regions.key?(border) }
end
graph
end
end
# US state borders example (partial)
map = MapColoring.new
map.add_region('California', ['Oregon', 'Nevada', 'Arizona'])
map.add_region('Oregon', ['California', 'Nevada', 'Idaho', 'Washington'])
map.add_region('Nevada', ['California', 'Oregon', 'Idaho', 'Utah', 'Arizona'])
map.add_region('Arizona', ['California', 'Nevada', 'Utah', 'New Mexico'])
map.add_region('Idaho', ['Oregon', 'Nevada', 'Utah', 'Wyoming', 'Montana', 'Washington'])
colors = map.color_map
# => {"California"=>"Red", "Oregon"=>"Blue", "Nevada"=>"Green", ...}
Register allocation in compilers assigns variables to limited processor registers. Variables with overlapping live ranges cannot share registers, creating an interference graph colored with available registers.
class CompilerRegisterAllocation
def initialize(num_registers)
@num_registers = num_registers
@live_ranges = {}
end
def add_variable(var, start_line, end_line)
@live_ranges[var] = (start_line..end_line)
end
def allocate
interference = build_interference_graph
coloring = GraphColoring.new(interference)
result = coloring.welsh_powell_coloring
if coloring.color_count <= @num_registers
assign_registers(result)
else
handle_spilling(result, coloring.color_count)
end
end
private
def build_interference_graph
graph = Graph.new
@live_ranges.keys.each { |var| graph.add_vertex(var) }
@live_ranges.keys.combination(2) do |var1, var2|
range1, range2 = @live_ranges[var1], @live_ranges[var2]
graph.add_edge(var1, var2) if ranges_overlap?(range1, range2)
end
graph
end
def ranges_overlap?(range1, range2)
range1.cover?(range2.first) || range2.cover?(range1.first)
end
def assign_registers(coloring)
registers = (0...@num_registers).map { |i| "R#{i}" }
coloring.transform_values { |color| registers[color - 1] }
end
def handle_spilling(coloring, needed_colors)
registers = (0...@num_registers).map { |i| "R#{i}" }
allocation = {}
spilled = []
coloring.each do |var, color|
if color <= @num_registers
allocation[var] = registers[color - 1]
else
spilled << var
allocation[var] = 'MEMORY'
end
end
{ allocation: allocation, spilled: spilled }
end
end
# Example usage
allocator = CompilerRegisterAllocation.new(4)
allocator.add_variable('a', 1, 5)
allocator.add_variable('b', 2, 7)
allocator.add_variable('c', 3, 6)
allocator.add_variable('d', 5, 8)
allocator.add_variable('e', 6, 9)
result = allocator.allocate
# => {"a"=>"R0", "b"=>"R1", "c"=>"R2", "d"=>"R0", "e"=>"R1"}
Frequency assignment in wireless networks allocates channels to transmitters, preventing interference between nearby stations. The constraint graph connects transmitters within interference range, requiring different frequencies.
class FrequencyAssignment
def initialize(transmitters, interference_distance)
@transmitters = transmitters # Hash of transmitter => [x, y] coordinates
@interference_distance = interference_distance
@channels = []
end
def assign_frequencies(available_channels)
@channels = available_channels
interference_graph = build_interference_graph
coloring = DSaturColoring.new(interference_graph)
channel_assignment = coloring.color
map_to_frequencies(channel_assignment)
end
private
def build_interference_graph
graph = Graph.new
@transmitters.keys.each { |t| graph.add_vertex(t) }
@transmitters.keys.combination(2) do |t1, t2|
distance = calculate_distance(@transmitters[t1], @transmitters[t2])
graph.add_edge(t1, t2) if distance <= @interference_distance
end
graph
end
def calculate_distance(point1, point2)
Math.sqrt((point1[0] - point2[0])**2 + (point1[1] - point2[1])**2)
end
def map_to_frequencies(coloring)
coloring.transform_values { |color| @channels[color - 1] }
end
end
# Example network
transmitters = {
'Tower1' => [0, 0],
'Tower2' => [3, 4],
'Tower3' => [6, 0],
'Tower4' => [3, -4],
'Tower5' => [10, 5]
}
assigner = FrequencyAssignment.new(transmitters, 6.0)
channels = ['Channel A', 'Channel B', 'Channel C', 'Channel D']
assignment = assigner.assign_frequencies(channels)
# => {"Tower1"=>"Channel A", "Tower2"=>"Channel B", "Tower3"=>"Channel A", ...}
Performance Considerations
Graph coloring complexity depends on both graph structure and solution quality requirements. Computing the chromatic number remains NP-complete, while finding any valid coloring (potentially suboptimal) admits polynomial-time solutions through greedy algorithms.
Greedy coloring achieves O(V + E) time complexity by processing each vertex once and checking neighbor colors. The Welsh-Powell variant adds O(V log V) sorting overhead but often reduces total colors used. DSATUR increases complexity to O(V²) due to dynamic vertex selection based on saturation degrees.
require 'benchmark'
def benchmark_coloring_methods(graph)
Benchmark.bm(20) do |x|
x.report('Greedy (natural):') do
1000.times { GraphColoring.new(graph).greedy_color }
end
x.report('Welsh-Powell:') do
1000.times { GraphColoring.new(graph).welsh_powell_coloring }
end
x.report('DSATUR:') do
1000.times { DSaturColoring.new(graph).color }
end
end
end
# Performance varies with graph density and structure
sparse_graph = generate_random_graph(100, 0.05)
dense_graph = generate_random_graph(100, 0.5)
puts "Sparse graph (5% density):"
benchmark_coloring_methods(sparse_graph)
puts "\nDense graph (50% density):"
benchmark_coloring_methods(dense_graph)
Backtracking algorithms exhibit exponential worst-case complexity O(k^V) where k represents the number of colors. Practical performance depends heavily on graph structure and pruning effectiveness. Graphs with large independent sets or low chromatic numbers prune search trees more aggressively.
Memory consumption scales with graph representation choice. Adjacency matrices require O(V²) space but provide O(1) edge lookup. Adjacency lists use O(V + E) space with O(degree(v)) neighbor access. For sparse graphs (E << V²), adjacency lists conserve memory significantly.
class GraphRepresentationComparison
def self.memory_analysis(num_vertices, edge_density)
num_edges = (num_vertices * (num_vertices - 1) / 2 * edge_density).to_i
# Adjacency matrix: V x V booleans
matrix_bytes = num_vertices * num_vertices
# Adjacency list: V vertex entries + 2E edge references
list_bytes = num_vertices * 40 + num_edges * 2 * 8 # Approximate Ruby object sizes
{
vertices: num_vertices,
edges: num_edges,
density: edge_density,
matrix_memory_kb: matrix_bytes / 1024.0,
list_memory_kb: list_bytes / 1024.0,
ratio: matrix_bytes.to_f / list_bytes
}
end
end
# Comparison across different densities
[0.01, 0.1, 0.5, 0.9].each do |density|
stats = GraphRepresentationComparison.memory_analysis(1000, density)
puts "Density #{density}: Matrix=#{stats[:matrix_memory_kb].round(2)}KB, " \
"List=#{stats[:list_memory_kb].round(2)}KB, Ratio=#{stats[:ratio].round(2)}"
end
Vertex ordering dramatically impacts greedy algorithm performance. Random orderings produce unpredictable results, while degree-based orderings provide consistent improvements. The largest-degree-first heuristic (Welsh-Powell) guarantees using at most max(d₁, d₂, ..., dₙ) + 1 colors where dᵢ represents vertex degrees in non-increasing order.
Parallel coloring algorithms partition the graph and color independent vertex sets concurrently. Jones-Plassmann algorithm assigns random priorities to vertices and colors maximal independent sets in parallel phases. The approach achieves speedup on multi-core systems but may use more colors than sequential algorithms.
require 'concurrent'
class ParallelColoring
def initialize(graph, thread_pool_size = 4)
@graph = graph
@pool = Concurrent::FixedThreadPool.new(thread_pool_size)
@coloring = Concurrent::Hash.new
end
def color
uncolored = @graph.vertices.to_set
color_num = 1
until uncolored.empty?
independent_set = find_maximal_independent_set(uncolored)
# Color independent set in parallel
futures = independent_set.map do |vertex|
Concurrent::Future.execute(executor: @pool) do
@coloring[vertex] = color_num
end
end
futures.each(&:wait)
uncolored -= independent_set
color_num += 1
end
@pool.shutdown
@pool.wait_for_termination
@coloring.to_h
end
private
def find_maximal_independent_set(vertices)
independent = Set.new
vertices = vertices.to_a.shuffle
vertices.each do |v|
neighbors = @graph.edges[v].to_set
independent << v unless independent.any? { |i| neighbors.include?(i) }
end
independent
end
end
Cache locality affects performance for large graphs. Breadth-first vertex orderings improve cache hits by coloring nearby vertices in sequence. Recursive backtracking algorithms benefit from tail-call optimization where available, though Ruby does not optimize tail calls by default.
Graph preprocessing reduces problem size through transformations. Identifying and removing simplicial vertices (vertices whose neighbors form a clique) decreases graph size while preserving chromatic number. Clique detection eliminates vertices that must receive distinct colors.
Common Patterns
Independent set coloring partitions vertices into color classes representing independent sets. Each iteration identifies a maximal independent set, assigns a new color to all vertices in the set, and removes them from consideration. The process continues until all vertices receive colors.
class IndependentSetColoring
def initialize(graph)
@graph = graph
end
def color
remaining = @graph.vertices.to_set
coloring = {}
color = 1
until remaining.empty?
independent_set = find_independent_set(remaining)
independent_set.each { |v| coloring[v] = color }
remaining -= independent_set
color += 1
end
coloring
end
private
def find_independent_set(vertices)
independent = Set.new
available = vertices.dup
until available.empty?
vertex = available.first
independent << vertex
# Remove vertex and its neighbors
available.delete(vertex)
@graph.edges[vertex].each { |neighbor| available.delete(neighbor) }
end
independent
end
end
Smallest-last ordering processes vertices in reverse order of removal when repeatedly removing minimum-degree vertices. This degeneracy-based ordering often outperforms simple degree-based approaches, particularly for graphs with small degeneracy.
class SmallestLastColoring
def initialize(graph)
@graph = graph
end
def color
ordering = compute_smallest_last_order
coloring = {}
ordering.each do |vertex|
neighbor_colors = @graph.edges[vertex].filter_map { |n| coloring[n] }.to_set
coloring[vertex] = (1..).find { |c| !neighbor_colors.include?(c) }
end
coloring
end
private
def compute_smallest_last_order
remaining = @graph.vertices.to_set
order = []
degrees = @graph.vertices.to_h { |v| [v, @graph.degree(v)] }
until remaining.empty?
# Select minimum degree vertex from remaining
vertex = remaining.min_by { |v| degrees[v] }
order.unshift(vertex)
remaining.delete(vertex)
# Update neighbor degrees
@graph.edges[vertex].each do |neighbor|
degrees[neighbor] -= 1 if remaining.include?(neighbor)
end
end
order
end
end
Iterated greedy improves solutions through repeated randomized coloring. The algorithm applies greedy coloring with random vertex orderings multiple times, retaining the best solution. This pattern balances solution quality and computation time effectively.
class IteratedGreedyColoring
def initialize(graph, iterations = 100)
@graph = graph
@iterations = iterations
end
def color
best_coloring = nil
best_colors = Float::INFINITY
@iterations.times do
ordering = @graph.vertices.shuffle
coloring = greedy_with_order(ordering)
colors_used = coloring.values.max
if colors_used < best_colors
best_colors = colors_used
best_coloring = coloring
end
end
best_coloring
end
private
def greedy_with_order(ordering)
coloring = {}
ordering.each do |vertex|
used = @graph.edges[vertex].filter_map { |n| coloring[n] }.to_set
coloring[vertex] = (1..).find { |c| !used.include?(c) }
end
coloring
end
end
Tabu search maintains a memory of recently explored solutions, preventing cycles in local search. The algorithm iteratively modifies the current coloring by recoloring vertices, avoiding recently used color-vertex combinations. This pattern escapes local optima more effectively than simple hill climbing.
Conflict-based recoloring targets vertices involved in color conflicts. When a partial k-coloring contains conflicts, the algorithm selects conflicting vertices and reassigns their colors. Kempe chains (alternating color sequences) enable conflict-free color swaps.
class KempeChainColoring
def initialize(graph)
@graph = graph
@coloring = {}
end
def improve_coloring(initial_coloring)
@coloring = initial_coloring.dup
max_color = @coloring.values.max
# Try to reduce colors used
(1..max_color).reverse_each do |target_color|
vertices_with_color = @coloring.select { |_, c| c == target_color }.keys
vertices_with_color.each do |vertex|
(1...target_color).each do |new_color|
if can_swap_via_kempe_chain?(vertex, target_color, new_color)
perform_kempe_swap(vertex, target_color, new_color)
break
end
end
end
end
@coloring
end
private
def can_swap_via_kempe_chain?(start_vertex, color1, color2)
# Build component of vertices with color1 or color2
component = find_kempe_component(start_vertex, color1, color2)
# Check if swapping colors in component creates conflicts
component.none? do |v|
new_color = @coloring[v] == color1 ? color2 : color1
@graph.edges[v].any? { |n| !component.include?(n) && @coloring[n] == new_color }
end
end
def find_kempe_component(start, color1, color2)
component = Set.new([start])
queue = [start]
until queue.empty?
vertex = queue.shift
@graph.edges[vertex].each do |neighbor|
next if component.include?(neighbor)
next unless [color1, color2].include?(@coloring[neighbor])
component << neighbor
queue << neighbor
end
end
component
end
def perform_kempe_swap(start_vertex, color1, color2)
component = find_kempe_component(start_vertex, color1, color2)
component.each do |vertex|
@coloring[vertex] = @coloring[vertex] == color1 ? color2 : color1
end
end
end
Clique-based bounds establish lower limits on chromatic number. Any clique (complete subgraph) requires |clique| distinct colors, providing a chromatic lower bound. Algorithms detecting maximum cliques tighten this bound, improving branch-and-bound pruning.
Reference
Complexity Classes
| Problem | Complexity | Best Known Algorithm |
|---|---|---|
| k-Coloring Decision | NP-complete | Backtracking with pruning |
| Chromatic Number | NP-hard | Branch-and-bound |
| 2-Coloring | O(V + E) | BFS/DFS bipartiteness check |
| Planar Graph Coloring | O(V) | Four-color theorem algorithms |
| Greedy Coloring | O(V + E) | Sequential assignment |
| Optimal Coloring | Exponential | Exhaustive search required |
Algorithm Comparison
| Algorithm | Time Complexity | Space | Colors Used | Best For |
|---|---|---|---|---|
| Greedy | O(V + E) | O(V) | ≤ Δ + 1 | Fast approximate solutions |
| Welsh-Powell | O(V log V + E) | O(V) | Often better than greedy | General purpose |
| DSATUR | O(V²) | O(V) | Often optimal for small graphs | Moderate-sized graphs |
| Backtracking | O(k^V) | O(V) | Optimal | Small graphs only |
| Branch-and-Bound | O(k^V) with pruning | O(V) | Optimal | Instances needing optimality |
| Smallest-Last | O(V + E) | O(V) | Related to degeneracy | Sparse graphs |
Graph Properties
| Property | Definition | Chromatic Implications |
|---|---|---|
| Clique Number ω(G) | Size of largest complete subgraph | χ(G) ≥ ω(G) |
| Chromatic Number χ(G) | Minimum colors needed | NP-hard to compute |
| Maximum Degree Δ(G) | Highest vertex degree | χ(G) ≤ Δ(G) + 1 |
| Degeneracy d | Smallest max degree in subgraphs | χ(G) ≤ d + 1 |
| Girth | Length of shortest cycle | Higher girth → harder coloring |
| Planar | Embeddable in plane | χ(G) ≤ 4 |
| Bipartite | Two independent sets | χ(G) = 2 |
| Perfect Graph | χ(H) = ω(H) for all induced H | Polynomial-time colorable |
Ruby Method Reference
| Method | Purpose | Returns |
|---|---|---|
| greedy_color(order) | Sequential greedy coloring | Hash of vertex to color |
| welsh_powell_coloring | Degree-ordered greedy | Hash of vertex to color |
| valid_coloring?(graph, coloring) | Checks constraint satisfaction | Boolean |
| chromatic_number(graph) | Finds minimum colors | Integer |
| max_degree | Computes maximum vertex degree | Integer |
| find_independent_set(vertices) | Extracts independent vertices | Set |
| kempe_chain_swap(v, c1, c2) | Attempts color reduction | Boolean |
Color Bound Formulas
| Bound Type | Formula | Notes |
|---|---|---|
| Clique Lower Bound | χ(G) ≥ ω(G) | Tight for perfect graphs |
| Degree Upper Bound | χ(G) ≤ Δ(G) + 1 | Greedy achievable |
| Brooks Theorem | χ(G) ≤ Δ(G) | For connected, non-complete, non-odd-cycle |
| Degeneracy Bound | χ(G) ≤ d + 1 | d is graph degeneracy |
| Fractional Chromatic | χ_f(G) ≤ χ(G) | Linear programming relaxation |
Common Graph Types
| Graph Type | Chromatic Number | Polynomial Algorithm |
|---|---|---|
| Complete K_n | n | Trivial |
| Bipartite | 2 | BFS coloring |
| Trees | 2 | DFS coloring |
| Cycles C_n | 2 if even, 3 if odd | Sequential coloring |
| Planar | ≤ 4 | Four-color theorem |
| Interval | ≤ ω(G) | Greedy on sorted intervals |
| Chordal | ω(G) | Perfect elimination ordering |
| Perfect | ω(G) | Polynomial-time algorithms exist |
Application Domains
| Domain | Graph Representation | Coloring Goal |
|---|---|---|
| Scheduling | Events as vertices, conflicts as edges | Minimize time slots |
| Register Allocation | Variables as vertices, interference as edges | Minimize registers used |
| Frequency Assignment | Transmitters as vertices, interference as edges | Minimize frequencies |
| Map Coloring | Regions as vertices, borders as edges | Distinguish adjacent regions |
| Sudoku | Cells as vertices, constraints as edges | Valid number placement |
| Exam Timetabling | Exams as vertices, student conflicts as edges | Minimize exam periods |
Performance Optimization Strategies
| Strategy | Impact | When to Apply |
|---|---|---|
| Vertex ordering | 2-10x fewer colors | Always with greedy |
| Preprocessing | Reduces problem size | Graphs with simplicial vertices |
| Kempe chain optimization | Reduces colors by 1-2 | After initial coloring |
| Parallel independent sets | Speedup on multi-core | Large graphs with parallelism |
| Clique detection | Better bounds | Branch-and-bound algorithms |
| Degeneracy ordering | Better than degree | Sparse graphs |