Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Enhance docs, add more tests in WelshPowell #5971

Merged
merged 3 commits into from
Oct 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,41 @@
import java.util.HashSet;
import java.util.stream.IntStream;

/*
* The Welsh-Powell algorithm is a graph coloring algorithm
* used for coloring a graph with the minimum number of colors.
* https://en.wikipedia.org/wiki/Graph_coloring
/**
* The Welsh-Powell algorithm is a graph coloring algorithm that aims to color a graph
* using the minimum number of colors such that no two adjacent vertices share the same color.
*
* <p>
* The algorithm works by:
* <ol>
* <li>Sorting the vertices in descending order based on their degrees (number of edges connected).</li>
* <li>Iterating through each vertex and assigning it the smallest available color that has not been used by its adjacent vertices.</li>
* <li>Coloring adjacent vertices with the same color is avoided.</li>
* </ol>
* </p>
*
* <p>
* For more information, see <a href="https://en.wikipedia.org/wiki/Graph_coloring">Graph Coloring</a>.
* </p>
*/

public final class WelshPowell {
private static final int BLANK_COLOR = -1; // Representing uncolored state
private static final int BLANK_COLOR = -1; // Constant representing an uncolored state

private WelshPowell() {
}

/**
* Represents a graph using an adjacency list.
*/
static final class Graph {
private HashSet<Integer>[] adjacencyLists;

private final HashSet<Integer>[] adjacencyLists;

/**
* Initializes a graph with a specified number of vertices.
*
* @param vertices the number of vertices in the graph
* @throws IllegalArgumentException if the number of vertices is negative
*/
private Graph(int vertices) {
if (vertices < 0) {
throw new IllegalArgumentException("Number of vertices cannot be negative");
Expand All @@ -29,6 +49,13 @@ private Graph(int vertices) {
Arrays.setAll(adjacencyLists, i -> new HashSet<>());
}

/**
* Adds an edge between two vertices in the graph.
*
* @param nodeA one end of the edge
* @param nodeB the other end of the edge
* @throws IllegalArgumentException if the vertices are out of bounds or if a self-loop is attempted
*/
private void addEdge(int nodeA, int nodeB) {
validateVertex(nodeA);
validateVertex(nodeB);
Expand All @@ -39,21 +66,46 @@ private void addEdge(int nodeA, int nodeB) {
adjacencyLists[nodeB].add(nodeA);
}

/**
* Validates that the vertex index is within the bounds of the graph.
*
* @param vertex the index of the vertex to validate
* @throws IllegalArgumentException if the vertex is out of bounds
*/
private void validateVertex(int vertex) {
if (vertex < 0 || vertex >= getNumVertices()) {
throw new IllegalArgumentException("Vertex " + vertex + " is out of bounds");
}
}

/**
* Returns the adjacency list for a specific vertex.
*
* @param vertex the index of the vertex
* @return the set of adjacent vertices
*/
HashSet<Integer> getAdjacencyList(int vertex) {
return adjacencyLists[vertex];
}

/**
* Returns the number of vertices in the graph.
*
* @return the number of vertices
*/
int getNumVertices() {
return adjacencyLists.length;
}
}

/**
* Creates a graph with the specified number of vertices and edges.
*
* @param numberOfVertices the total number of vertices
* @param listOfEdges a 2D array representing edges where each inner array contains two vertex indices
* @return a Graph object representing the created graph
* @throws IllegalArgumentException if the edge array is invalid or vertices are out of bounds
*/
public static Graph makeGraph(int numberOfVertices, int[][] listOfEdges) {
Graph graph = new Graph(numberOfVertices);
for (int[] edge : listOfEdges) {
Expand All @@ -65,6 +117,12 @@ public static Graph makeGraph(int numberOfVertices, int[][] listOfEdges) {
return graph;
}

/**
* Finds the coloring of the given graph using the Welsh-Powell algorithm.
*
* @param graph the input graph to color
* @return an array of integers where each index represents a vertex and the value represents the color assigned
*/
public static int[] findColoring(Graph graph) {
int[] colors = initializeColors(graph.getNumVertices());
Integer[] sortedVertices = getSortedNodes(graph);
Expand All @@ -83,30 +141,70 @@ public static int[] findColoring(Graph graph) {
return colors;
}

/**
* Helper method to check if a color is unassigned
*
* @param color the color to check
* @return {@code true} if the color is unassigned, {@code false} otherwise
*/
private static boolean isBlank(int color) {
return color == BLANK_COLOR;
}

/**
* Checks if a vertex has adjacent colored vertices
*
* @param graph the input graph
* @param vertex the vertex to check
* @param colors the array of colors assigned to the vertices
* @return {@code true} if the vertex has adjacent colored vertices, {@code false} otherwise
*/
private static boolean isAdjacentToColored(Graph graph, int vertex, int[] colors) {
return graph.getAdjacencyList(vertex).stream().anyMatch(otherVertex -> !isBlank(colors[otherVertex]));
}

/**
* Initializes the colors array with blank color
*
* @param numberOfVertices the number of vertices in the graph
* @return an array of integers representing the colors assigned to the vertices
*/
private static int[] initializeColors(int numberOfVertices) {
int[] colors = new int[numberOfVertices];
Arrays.fill(colors, BLANK_COLOR);
return colors;
}

/**
* Sorts the vertices by their degree in descending order
*
* @param graph the input graph
* @return an array of integers representing the vertices sorted by degree
*/
private static Integer[] getSortedNodes(final Graph graph) {
return IntStream.range(0, graph.getNumVertices()).boxed().sorted(Comparator.comparingInt(v -> - graph.getAdjacencyList(v).size())).toArray(Integer[] ::new);
}

/**
* Computes the colors already used by the adjacent vertices
*
* @param graph the input graph
* @param vertex the vertex to check
* @param colors the array of colors assigned to the vertices
* @return an array of booleans representing the colors used by the adjacent vertices
*/
private static boolean[] computeUsedColors(final Graph graph, final int vertex, final int[] colors) {
boolean[] usedColors = new boolean[graph.getNumVertices()];
graph.getAdjacencyList(vertex).stream().map(neighbor -> colors[neighbor]).filter(color -> !isBlank(color)).forEach(color -> usedColors[color] = true);
return usedColors;
}

/**
* Finds the first unused color
*
* @param usedColors the array of colors used by the adjacent vertices
* @return the first unused color
*/
private static int firstUnusedColor(boolean[] usedColors) {
return IntStream.range(0, usedColors.length).filter(color -> !usedColors[color]).findFirst().getAsInt();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,26 +34,25 @@ void testCompleteGraph() {
assertEquals(3, countDistinctColors(colors));
}

// The following test originates from the following website : https://www.geeksforgeeks.org/welsh-powell-graph-colouring-algorithm/
@Test
void testComplexGraph() {
int[][] edges = {
{0, 7}, // A-H
{0, 1}, // A-B
{1, 3}, // B-D
{2, 3}, // C-D
{3, 8}, // D-I
{3, 10}, // D-K
{4, 10}, // E-K
{4, 5}, // E-F
{5, 6}, // F-G
{6, 10}, // G-K
{6, 7}, // G-H
{7, 8}, // H-I
{7, 9}, // H-J
{7, 10}, // H-K
{8, 9}, // I-J
{9, 10}, // J-K
{0, 7},
{0, 1},
{1, 3},
{2, 3},
{3, 8},
{3, 10},
{4, 10},
{4, 5},
{5, 6},
{6, 10},
{6, 7},
{7, 8},
{7, 9},
{7, 10},
{8, 9},
{9, 10},
};

final var graph = WelshPowell.makeGraph(11, edges); // 11 vertices from A (0) to K (10)
Expand Down Expand Up @@ -86,24 +85,35 @@ void testInvalidEdgeArray() {

@Test
void testWithPreColoredVertex() {
// Create a linear graph with 4 vertices and edges connecting them in sequence
final var graph = WelshPowell.makeGraph(4, new int[][] {{0, 1}, {1, 2}, {2, 3}});

// Apply the Welsh-Powell coloring algorithm to the graph
int[] colors = WelshPowell.findColoring(graph);

// Validate that the coloring is correct (no two adjacent vertices have the same color)
assertTrue(isColoringValid(graph, colors));

// Check if the algorithm has used at least 2 colors (expected for a linear graph)
assertTrue(countDistinctColors(colors) >= 2);

// Verify that all vertices have been assigned a color
for (int color : colors) {
assertTrue(color >= 0);
}
}

@Test
void testLargeGraph() {
int[][] edges = {{0, 1}, {1, 2}, {2, 3}, {3, 4}, {4, 5}, {5, 0}, {6, 7}, {7, 8}, {8, 6}, {9, 10}, {10, 11}, {11, 9}, {12, 13}, {13, 14}, {14, 15}};

final var graph = WelshPowell.makeGraph(16, edges); // 16 vertices
int[] colors = WelshPowell.findColoring(graph);
assertTrue(isColoringValid(graph, colors));
assertEquals(3, countDistinctColors(colors)); // Expecting a maximum of 3 colors
}

@Test
void testStarGraph() {
int[][] edges = {{0, 1}, {0, 2}, {0, 3}, {0, 4}};

final var graph = WelshPowell.makeGraph(5, edges); // 5 vertices in a star formation
int[] colors = WelshPowell.findColoring(graph);
assertTrue(isColoringValid(graph, colors));
assertEquals(2, countDistinctColors(colors)); // Star graph can be colored with 2 colors
}

private boolean isColoringValid(Graph graph, int[] colors) {
if (Arrays.stream(colors).anyMatch(n -> n < 0)) {
return false;
Expand Down