Skip to content
This repository was archived by the owner on Jun 18, 2026. It is now read-only.

Commit 66f9110

Browse files
feat: Graph Coloring — Welsh-Powell greedy vertex coloring
Add GraphColoringAnalyzer for proper vertex coloring using the Welsh-Powell algorithm (greedy by decreasing degree). Assigns colors to vertices so no two adjacent vertices share the same color. GraphColoringAnalyzer: - compute() — Welsh-Powell coloring: sorts vertices by degree descending, assigns smallest available color greedily - computeWithOrder(List) — custom vertex ordering for strategy comparison (e.g., random vs degree-based) - Validation: verifies no adjacent vertices share a color - Thread-safe immutable results ColoringResult: - getColorAssignment() — vertex-to-color map (0-indexed integers) - getColor(vertex) — single vertex lookup (-1 if not found) - getColorClasses() — color index to vertex list (sorted) - getVerticesWithColor(color) — vertices sharing a color - getChromaticBound() — upper bound on chromatic number - getLargestClassSize() / getSmallestClassSize() — class analytics - getSummary() — key metrics map - toString() — human-readable summary with class details - Immutable maps (unmodifiable wrappers) Applications: scheduling (exams, meetings), register allocation, frequency assignment, map coloring, resource conflict resolution. 39 new tests covering: null/empty/single/pair graphs, triangle (K3), path graph, even/odd cycles, complete graph (K4), star, K3,3 bipartite, Petersen graph (chromatic 3), disconnected components, isolated vertices, mixed edge types, color class partitioning, class size analytics, summary/toString, custom ordering, Welsh-Powell degree priority, validation property (exhaustive adjacency check), result immutability.
1 parent 25010b5 commit 66f9110

2 files changed

Lines changed: 1001 additions & 0 deletions

File tree

Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
1+
package gvisual;
2+
3+
import edu.uci.ics.jung.graph.Graph;
4+
import java.util.*;
5+
6+
/**
7+
* Graph coloring using the Welsh-Powell algorithm -- a greedy heuristic
8+
* that assigns colors to vertices so no two adjacent vertices share the
9+
* same color. Vertices are processed in decreasing order of degree, which
10+
* typically produces fewer colors than naive greedy approaches.
11+
*
12+
* <p>Applications include scheduling (exams, meetings), register allocation,
13+
* frequency assignment, and map coloring. The number of colors used is an
14+
* upper bound on the chromatic number.</p>
15+
*
16+
* <p>Usage:</p>
17+
* <pre>
18+
* GraphColoringAnalyzer analyzer = new GraphColoringAnalyzer(graph);
19+
* GraphColoringAnalyzer.ColoringResult result = analyzer.compute();
20+
* int colors = result.getChromaticBound();
21+
* Map&lt;String, Integer&gt; assignment = result.getColorAssignment();
22+
* </pre>
23+
*
24+
* @author zalenix
25+
*/
26+
public class GraphColoringAnalyzer {
27+
28+
private final Graph<String, edge> graph;
29+
30+
/**
31+
* Creates a new GraphColoringAnalyzer for the given graph.
32+
*
33+
* @param graph the JUNG graph to color
34+
* @throws IllegalArgumentException if graph is null
35+
*/
36+
public GraphColoringAnalyzer(Graph<String, edge> graph) {
37+
if (graph == null) {
38+
throw new IllegalArgumentException("Graph must not be null");
39+
}
40+
this.graph = graph;
41+
}
42+
43+
/**
44+
* Computes a proper vertex coloring using Welsh-Powell (greedy by
45+
* decreasing degree). Colors are integers starting at 0.
46+
*
47+
* @return a ColoringResult with the assignment and analytics
48+
*/
49+
public ColoringResult compute() {
50+
Collection<String> vertices = graph.getVertices();
51+
int n = vertices.size();
52+
53+
if (n == 0) {
54+
return new ColoringResult(
55+
Collections.emptyMap(),
56+
Collections.emptyMap(),
57+
0, 0, true
58+
);
59+
}
60+
61+
// Sort vertices by degree descending, break ties alphabetically
62+
List<String> sorted = new ArrayList<>(vertices);
63+
sorted.sort((a, b) -> {
64+
int cmp = Integer.compare(graph.degree(b), graph.degree(a));
65+
return cmp != 0 ? cmp : a.compareTo(b);
66+
});
67+
68+
Map<String, Integer> colorAssignment = new HashMap<>();
69+
int maxColor = -1;
70+
71+
for (String vertex : sorted) {
72+
// Find colors used by neighbors
73+
Set<Integer> usedColors = new HashSet<>();
74+
for (String neighbor : graph.getNeighbors(vertex)) {
75+
Integer neighborColor = colorAssignment.get(neighbor);
76+
if (neighborColor != null) {
77+
usedColors.add(neighborColor);
78+
}
79+
}
80+
81+
// Assign the smallest available color
82+
int color = 0;
83+
while (usedColors.contains(color)) {
84+
color++;
85+
}
86+
colorAssignment.put(vertex, color);
87+
if (color > maxColor) {
88+
maxColor = color;
89+
}
90+
}
91+
92+
int chromaticBound = maxColor + 1;
93+
94+
// Build color classes (which vertices share each color)
95+
Map<Integer, List<String>> colorClasses = new HashMap<>();
96+
for (int c = 0; c < chromaticBound; c++) {
97+
colorClasses.put(c, new ArrayList<>());
98+
}
99+
for (Map.Entry<String, Integer> entry : colorAssignment.entrySet()) {
100+
colorClasses.get(entry.getValue()).add(entry.getKey());
101+
}
102+
// Sort each class for deterministic output
103+
for (List<String> cls : colorClasses.values()) {
104+
Collections.sort(cls);
105+
}
106+
107+
boolean valid = validate(colorAssignment);
108+
109+
return new ColoringResult(colorAssignment, colorClasses, chromaticBound, n, valid);
110+
}
111+
112+
/**
113+
* Validates that no two adjacent vertices share the same color.
114+
*
115+
* @param assignment vertex-to-color mapping
116+
* @return true if the coloring is proper
117+
*/
118+
private boolean validate(Map<String, Integer> assignment) {
119+
for (edge e : graph.getEdges()) {
120+
String v1 = graph.getEndpoints(e).getFirst();
121+
String v2 = graph.getEndpoints(e).getSecond();
122+
Integer c1 = assignment.get(v1);
123+
Integer c2 = assignment.get(v2);
124+
if (c1 != null && c2 != null && c1.equals(c2)) {
125+
return false;
126+
}
127+
}
128+
return true;
129+
}
130+
131+
/**
132+
* Computes a coloring using a specific vertex ordering instead of
133+
* Welsh-Powell's degree ordering. Useful for comparing strategies.
134+
*
135+
* @param vertexOrder the order in which to process vertices
136+
* @return a ColoringResult with the assignment
137+
* @throws IllegalArgumentException if vertexOrder is null or contains
138+
* vertices not in the graph
139+
*/
140+
public ColoringResult computeWithOrder(List<String> vertexOrder) {
141+
if (vertexOrder == null) {
142+
throw new IllegalArgumentException("Vertex order must not be null");
143+
}
144+
145+
for (String v : vertexOrder) {
146+
if (!graph.containsVertex(v)) {
147+
throw new IllegalArgumentException(
148+
"Vertex not in graph: " + v);
149+
}
150+
}
151+
152+
Map<String, Integer> colorAssignment = new HashMap<>();
153+
int maxColor = -1;
154+
155+
for (String vertex : vertexOrder) {
156+
Set<Integer> usedColors = new HashSet<>();
157+
for (String neighbor : graph.getNeighbors(vertex)) {
158+
Integer neighborColor = colorAssignment.get(neighbor);
159+
if (neighborColor != null) {
160+
usedColors.add(neighborColor);
161+
}
162+
}
163+
164+
int color = 0;
165+
while (usedColors.contains(color)) {
166+
color++;
167+
}
168+
colorAssignment.put(vertex, color);
169+
if (color > maxColor) {
170+
maxColor = color;
171+
}
172+
}
173+
174+
int chromaticBound = maxColor + 1;
175+
176+
Map<Integer, List<String>> colorClasses = new HashMap<>();
177+
for (int c = 0; c < chromaticBound; c++) {
178+
colorClasses.put(c, new ArrayList<>());
179+
}
180+
for (Map.Entry<String, Integer> entry : colorAssignment.entrySet()) {
181+
colorClasses.get(entry.getValue()).add(entry.getKey());
182+
}
183+
for (List<String> cls : colorClasses.values()) {
184+
Collections.sort(cls);
185+
}
186+
187+
boolean valid = validate(colorAssignment);
188+
int n = colorAssignment.size();
189+
190+
return new ColoringResult(colorAssignment, colorClasses, chromaticBound, n, valid);
191+
}
192+
193+
// =============================================
194+
// Result class
195+
// =============================================
196+
197+
/**
198+
* Holds the results of a graph coloring computation.
199+
*/
200+
public static class ColoringResult {
201+
202+
private final Map<String, Integer> colorAssignment;
203+
private final Map<Integer, List<String>> colorClasses;
204+
private final int chromaticBound;
205+
private final int vertexCount;
206+
private final boolean valid;
207+
208+
ColoringResult(
209+
Map<String, Integer> colorAssignment,
210+
Map<Integer, List<String>> colorClasses,
211+
int chromaticBound,
212+
int vertexCount,
213+
boolean valid) {
214+
this.colorAssignment = Collections.unmodifiableMap(colorAssignment);
215+
this.colorClasses = Collections.unmodifiableMap(colorClasses);
216+
this.chromaticBound = chromaticBound;
217+
this.vertexCount = vertexCount;
218+
this.valid = valid;
219+
}
220+
221+
/**
222+
* Returns the vertex-to-color assignment. Colors are 0-indexed
223+
* integers.
224+
*/
225+
public Map<String, Integer> getColorAssignment() {
226+
return colorAssignment;
227+
}
228+
229+
/**
230+
* Returns the color of a specific vertex, or -1 if not found.
231+
*/
232+
public int getColor(String vertex) {
233+
Integer c = colorAssignment.get(vertex);
234+
return c != null ? c : -1;
235+
}
236+
237+
/**
238+
* Returns the color classes -- a map from color index to the
239+
* list of vertices assigned that color.
240+
*/
241+
public Map<Integer, List<String>> getColorClasses() {
242+
return colorClasses;
243+
}
244+
245+
/**
246+
* Returns the vertices assigned to a specific color, or an
247+
* empty list if the color index is invalid.
248+
*/
249+
public List<String> getVerticesWithColor(int color) {
250+
List<String> list = colorClasses.get(color);
251+
return list != null ? list : Collections.emptyList();
252+
}
253+
254+
/**
255+
* Returns the upper bound on the chromatic number (number of
256+
* colors used). The actual chromatic number may be lower.
257+
*/
258+
public int getChromaticBound() {
259+
return chromaticBound;
260+
}
261+
262+
/**
263+
* Returns the number of vertices that were colored.
264+
*/
265+
public int getVertexCount() {
266+
return vertexCount;
267+
}
268+
269+
/**
270+
* Returns true if the coloring is valid (no adjacent vertices
271+
* share a color).
272+
*/
273+
public boolean isValid() {
274+
return valid;
275+
}
276+
277+
/**
278+
* Returns the size of the largest color class.
279+
*/
280+
public int getLargestClassSize() {
281+
int max = 0;
282+
for (List<String> cls : colorClasses.values()) {
283+
if (cls.size() > max) {
284+
max = cls.size();
285+
}
286+
}
287+
return max;
288+
}
289+
290+
/**
291+
* Returns the size of the smallest color class.
292+
*/
293+
public int getSmallestClassSize() {
294+
if (colorClasses.isEmpty()) {
295+
return 0;
296+
}
297+
int min = Integer.MAX_VALUE;
298+
for (List<String> cls : colorClasses.values()) {
299+
if (cls.size() < min) {
300+
min = cls.size();
301+
}
302+
}
303+
return min;
304+
}
305+
306+
/**
307+
* Returns a summary map with key metrics.
308+
*/
309+
public Map<String, Object> getSummary() {
310+
Map<String, Object> summary = new LinkedHashMap<>();
311+
summary.put("vertexCount", vertexCount);
312+
summary.put("chromaticBound", chromaticBound);
313+
summary.put("valid", valid);
314+
summary.put("largestClass", getLargestClassSize());
315+
summary.put("smallestClass", getSmallestClassSize());
316+
317+
Map<Integer, Integer> classSizes = new LinkedHashMap<>();
318+
for (Map.Entry<Integer, List<String>> entry : colorClasses.entrySet()) {
319+
classSizes.put(entry.getKey(), entry.getValue().size());
320+
}
321+
summary.put("classSizes", classSizes);
322+
323+
return summary;
324+
}
325+
326+
/**
327+
* Returns a human-readable summary string.
328+
*/
329+
@Override
330+
public String toString() {
331+
StringBuilder sb = new StringBuilder();
332+
sb.append("Graph Coloring Result\n");
333+
sb.append("--------------------\n");
334+
sb.append(String.format("Vertices: %d%n", vertexCount));
335+
sb.append(String.format("Colors used (chromatic bound): %d%n", chromaticBound));
336+
sb.append(String.format("Valid coloring: %s%n", valid));
337+
sb.append(String.format("Largest color class: %d%n", getLargestClassSize()));
338+
sb.append(String.format("Smallest color class: %d%n", getSmallestClassSize()));
339+
sb.append("\nColor classes:\n");
340+
for (Map.Entry<Integer, List<String>> entry : colorClasses.entrySet()) {
341+
sb.append(String.format(" Color %d (%d vertices): %s%n",
342+
entry.getKey(), entry.getValue().size(), entry.getValue()));
343+
}
344+
return sb.toString();
345+
}
346+
}
347+
}

0 commit comments

Comments
 (0)