Graph coloring
Colouring assigns colours to vertices so that no edge joins two equal colours. Two colours are decided by BFS, but the general chromatic number is NP-hard, so practice relies on greedy rules, DSATUR and exact search for small graphs.
Learning goals
- Test 2-colourability with BFS
- Write a greedy colouring and explain its dependence on vertex order
- Use DSATUR for better colourings
- Find the exact chromatic number by backtracking and know its complexity
Prerequisites
Scheduling exams so that no student has two at once, assigning radio frequencies to nearby towers, allocating registers in a compiler: each is a graph colouring problem. Conflicts are edges, colours are time slots, frequencies or registers, and fewer colours mean a cheaper solution.
Bipartite 2-coloring
A **proper colouring** gives each vertex a colour so that adjacent vertices differ. The smallest number of colours is the **chromatic number** χ(G). A graph is 2-colourable exactly when it is **bipartite**, and that is the one case that is easy: it has no odd cycle.
Run a BFS from every unvisited vertex, giving the start colour 0 and each newly found neighbour the opposite colour. If some edge joins two vertices with the same colour, the BFS tree paths together with that edge form an odd cycle, and no 2-colouring exists. Otherwise the two colour classes are the two sides. The code returns the colouring or null, in O(V + E).
In the animation choose the first method. The “crown” graph (vertices 0 to 5, edges 0-3, 0-5, 2-1, 2-5, 4-1, 4-3) is bipartite with sides {0, 2, 4} and {1, 3, 5}; the BFS finds them. On the triangle preset the BFS stops at edge 1-2, where both ends have colour 1, and reports an odd cycle.
1@SuppressWarnings("unchecked")2static List<Integer>[] adjacency(int n, int[][] edges) {3List<Integer>[] adj = new List[n];4for (int i = 0; i < n; i++) adj[i] = new ArrayList<>();5for (int[] e : edges) { adj[e[0]].add(e[1]); adj[e[1]].add(e[0]); }6return adj;7}8static int[] bipartiteColouring(int n, List<Integer>[] adj) { // 2-colouring by BFS, or null when an odd cycle exists9int[] colour = new int[n];10Arrays.fill(colour, -1);11for (int s = 0; s < n; s++) {12if (colour[s] != -1) continue;13colour[s] = 0;14Deque<Integer> queue = new ArrayDeque<>(List.of(s));15while (!queue.isEmpty()) {16int u = queue.poll();17for (int v : adj[u]) {18if (colour[v] == colour[u]) return null; // both ends of an edge got the same colour19if (colour[v] == -1) { colour[v] = 1 - colour[u]; queue.add(v); }20}21}22}23return colour;24}
Worst
O(V + E)
Space
O(V)
Graph colouring: 2-colouring, greedy, DSATUR and the exact chromatic number
Interactive · edit the inputundirected graph · 6 vertices · 6 edges
colour of each vertex
Greedy colouring: take the vertices in index order and give each the smallest colour that none of its already coloured neighbours uses. It never needs more than (maximum degree + 1) colours, but the order decides how many it really uses.
1@SuppressWarnings("unchecked")2static List<Integer>[] adjacency(int n, int[][] edges) {3List<Integer>[] adj = new List[n];4for (int i = 0; i < n; i++) adj[i] = new ArrayList<>();5for (int[] e : edges) { adj[e[0]].add(e[1]); adj[e[1]].add(e[0]); }6return adj;7}8static int[] bipartiteColouring(int n, List<Integer>[] adj) { // 2-colouring by BFS, or null when an odd cycle exists9int[] colour = new int[n];10Arrays.fill(colour, -1);11for (int s = 0; s < n; s++) {12if (colour[s] != -1) continue;13colour[s] = 0;14Deque<Integer> queue = new ArrayDeque<>(List.of(s));15while (!queue.isEmpty()) {16int u = queue.poll();17for (int v : adj[u]) {18if (colour[v] == colour[u]) return null; // both ends of an edge got the same colour19if (colour[v] == -1) { colour[v] = 1 - colour[u]; queue.add(v); }20}21}22}23return colour;24}25static int[] greedyColouring(int n, List<Integer>[] adj) { // vertices in index order, smallest free colour26int[] colour = new int[n];27Arrays.fill(colour, -1);28for (int v = 0; v < n; v++) {29boolean[] taken = new boolean[n + 1];30for (int u : adj[v]) if (colour[u] != -1) taken[colour[u]] = true;31int c = 0;32while (taken[c]) c++;33colour[v] = c;34}35return colour;36}37static int[] dsatur(int n, List<Integer>[] adj) { // always colour the most constrained vertex next38int[] colour = new int[n];39Arrays.fill(colour, -1);40for (int step = 0; step < n; step++) {41int pick = -1, bestSat = -1;42for (int v = 0; v < n; v++) {43if (colour[v] != -1) continue;44Set<Integer> seen = new HashSet<>(); // saturation: distinct colours among neighbours45for (int u : adj[v]) if (colour[u] != -1) seen.add(colour[u]);46if (seen.size() > bestSat || (seen.size() == bestSat && adj[v].size() > adj[pick].size())) { pick = v; bestSat = seen.size(); }47}48boolean[] taken = new boolean[n + 1];49for (int u : adj[pick]) if (colour[u] != -1) taken[colour[u]] = true;50int c = 0;51while (taken[c]) c++;52colour[pick] = c;53}54return colour;55}56static boolean tryColour(int v, int k, int[] colour, List<Integer>[] adj, int used) {57if (v == colour.length) return true;58for (int c = 0; c < k && c <= used; c++) { // c <= used: never open a colour before the one below it59boolean ok = true;60for (int u : adj[v]) if (u < v && colour[u] == c) ok = false;61if (!ok) continue;62colour[v] = c;63if (tryColour(v + 1, k, colour, adj, Math.max(used, c + 1))) return true;64}65colour[v] = -1;66return false;67}68static int chromaticNumber(int n, List<Integer>[] adj) { // smallest k that admits a proper k-colouring69for (int k = 1; k <= n; k++) {70int[] colour = new int[n];71if (tryColour(0, k, colour, adj, 0)) return k;72}73return n;74}
Live state
n
6
edges
6
Execution trace (dry run table)Show 8 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L25 | n=6 edges=6 | Greedy colouring: take the vertices in index order and give each the smallest colour that none of its already coloured neighbours uses. It never needs more than (maximum degree + 1) colours, but the order decides how many it really uses. |
| 2 | L33 | vertex=0 colour=0 colours_used=1 | Vertex 0: neighbours already use no colour, so the smallest free colour is 0. |
| 3 | L33 | vertex=1 colour=0 colours_used=1 | Vertex 1: neighbours already use no colour, so the smallest free colour is 0. |
| 4 | L33 | vertex=2 colour=1 colours_used=2 | Vertex 2: neighbours already use {0}, so the smallest free colour is 1. |
| 5 | L33 | vertex=3 colour=1 colours_used=2 | Vertex 3: neighbours already use {0}, so the smallest free colour is 1. |
| 6 | L33 | vertex=4 colour=2 colours_used=3 | Vertex 4: neighbours already use {0,1}, so the smallest free colour is 2. |
| 7 | L33 | vertex=5 colour=2 colours_used=3 | Vertex 5: neighbours already use {0,1}, so the smallest free colour is 2. |
| 8 | L35 | return=3 colouring=0:0 1:0 2:1 3:1 4:2 5:2 proper=yes | 3 colours used (greedy): 0:0 1:0 2:1 3:1 4:2 5:2. The colouring is proper, but it is only an upper bound on the chromatic number. |
Greedy coloring
**Greedy colouring** takes the vertices in some order and gives each the smallest colour not used by its already-coloured neighbours. It runs in O(V + E) and uses at most Δ + 1 colours, where Δ is the maximum degree, because a vertex with d ≤ Δ neighbours always has a free colour among the first d + 1.
The order decides everything. Greedy is optimal for some order of the vertices, but a bad order can be far from optimal even on a bipartite graph. The crown graph shows it: in index order greedy gives vertices 0 and 1 colour 0, then 2 and 3 colour 1, then 4 and 5 colour 2, so it uses 3 colours although the graph is bipartite and 2 suffice. Run the second method to watch this.
A better order helps: colour high-degree vertices first (Welsh-Powell), or use the order produced by degeneracy (smallest-last), which guarantees at most degeneracy + 1 colours and is excellent on planar graphs (at most 6 colours).
1static int[] greedyColouring(int n, List<Integer>[] adj) { // vertices in index order, smallest free colour2int[] colour = new int[n];3Arrays.fill(colour, -1);4for (int v = 0; v < n; v++) {5boolean[] taken = new boolean[n + 1];6for (int u : adj[v]) if (colour[u] != -1) taken[colour[u]] = true;7int c = 0;8while (taken[c]) c++;9colour[v] = c;10}11return colour;12}
Worst
O(V + E) (the code below is O(V · n) because of the boolean array per vertex)
Space
O(V)
Graph colouring: 2-colouring, greedy, DSATUR and the exact chromatic number
Interactive · edit the inputundirected graph · 6 vertices · 6 edges
colour of each vertex
Greedy colouring: take the vertices in index order and give each the smallest colour that none of its already coloured neighbours uses. It never needs more than (maximum degree + 1) colours, but the order decides how many it really uses.
1@SuppressWarnings("unchecked")2static List<Integer>[] adjacency(int n, int[][] edges) {3List<Integer>[] adj = new List[n];4for (int i = 0; i < n; i++) adj[i] = new ArrayList<>();5for (int[] e : edges) { adj[e[0]].add(e[1]); adj[e[1]].add(e[0]); }6return adj;7}8static int[] bipartiteColouring(int n, List<Integer>[] adj) { // 2-colouring by BFS, or null when an odd cycle exists9int[] colour = new int[n];10Arrays.fill(colour, -1);11for (int s = 0; s < n; s++) {12if (colour[s] != -1) continue;13colour[s] = 0;14Deque<Integer> queue = new ArrayDeque<>(List.of(s));15while (!queue.isEmpty()) {16int u = queue.poll();17for (int v : adj[u]) {18if (colour[v] == colour[u]) return null; // both ends of an edge got the same colour19if (colour[v] == -1) { colour[v] = 1 - colour[u]; queue.add(v); }20}21}22}23return colour;24}25static int[] greedyColouring(int n, List<Integer>[] adj) { // vertices in index order, smallest free colour26int[] colour = new int[n];27Arrays.fill(colour, -1);28for (int v = 0; v < n; v++) {29boolean[] taken = new boolean[n + 1];30for (int u : adj[v]) if (colour[u] != -1) taken[colour[u]] = true;31int c = 0;32while (taken[c]) c++;33colour[v] = c;34}35return colour;36}37static int[] dsatur(int n, List<Integer>[] adj) { // always colour the most constrained vertex next38int[] colour = new int[n];39Arrays.fill(colour, -1);40for (int step = 0; step < n; step++) {41int pick = -1, bestSat = -1;42for (int v = 0; v < n; v++) {43if (colour[v] != -1) continue;44Set<Integer> seen = new HashSet<>(); // saturation: distinct colours among neighbours45for (int u : adj[v]) if (colour[u] != -1) seen.add(colour[u]);46if (seen.size() > bestSat || (seen.size() == bestSat && adj[v].size() > adj[pick].size())) { pick = v; bestSat = seen.size(); }47}48boolean[] taken = new boolean[n + 1];49for (int u : adj[pick]) if (colour[u] != -1) taken[colour[u]] = true;50int c = 0;51while (taken[c]) c++;52colour[pick] = c;53}54return colour;55}56static boolean tryColour(int v, int k, int[] colour, List<Integer>[] adj, int used) {57if (v == colour.length) return true;58for (int c = 0; c < k && c <= used; c++) { // c <= used: never open a colour before the one below it59boolean ok = true;60for (int u : adj[v]) if (u < v && colour[u] == c) ok = false;61if (!ok) continue;62colour[v] = c;63if (tryColour(v + 1, k, colour, adj, Math.max(used, c + 1))) return true;64}65colour[v] = -1;66return false;67}68static int chromaticNumber(int n, List<Integer>[] adj) { // smallest k that admits a proper k-colouring69for (int k = 1; k <= n; k++) {70int[] colour = new int[n];71if (tryColour(0, k, colour, adj, 0)) return k;72}73return n;74}
Live state
n
6
edges
6
Execution trace (dry run table)Show 8 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L25 | n=6 edges=6 | Greedy colouring: take the vertices in index order and give each the smallest colour that none of its already coloured neighbours uses. It never needs more than (maximum degree + 1) colours, but the order decides how many it really uses. |
| 2 | L33 | vertex=0 colour=0 colours_used=1 | Vertex 0: neighbours already use no colour, so the smallest free colour is 0. |
| 3 | L33 | vertex=1 colour=0 colours_used=1 | Vertex 1: neighbours already use no colour, so the smallest free colour is 0. |
| 4 | L33 | vertex=2 colour=1 colours_used=2 | Vertex 2: neighbours already use {0}, so the smallest free colour is 1. |
| 5 | L33 | vertex=3 colour=1 colours_used=2 | Vertex 3: neighbours already use {0}, so the smallest free colour is 1. |
| 6 | L33 | vertex=4 colour=2 colours_used=3 | Vertex 4: neighbours already use {0,1}, so the smallest free colour is 2. |
| 7 | L33 | vertex=5 colour=2 colours_used=3 | Vertex 5: neighbours already use {0,1}, so the smallest free colour is 2. |
| 8 | L35 | return=3 colouring=0:0 1:0 2:1 3:1 4:2 5:2 proper=yes | 3 colours used (greedy): 0:0 1:0 2:1 3:1 4:2 5:2. The colouring is proper, but it is only an upper bound on the chromatic number. |
Chromatic number
The **chromatic number** is found exactly by asking, for k = 1, 2, 3, …, whether a proper colouring with at most k colours exists, and stopping at the first success. For one k, backtracking assigns colours to the vertices in order and undoes a choice when a vertex has no allowed colour left.
The code uses a standard symmetry break: a vertex may open colour c only when colours 0 … c − 1 are already in use, so permuting colour names does not produce duplicate searches. The search is exponential in the worst case, which is expected: deciding whether χ(G) ≤ 3 is already NP-complete.
In the animation the third method colours the 5-cycle with one chord: k = 1 and k = 2 fail, and k = 3 succeeds (8 assignments, 5 backtracks). For the complete graph K4 every smaller k fails and χ = 4 (10 assignments, 9 backtracks). Lower bounds come from cliques (χ ≥ ω); upper bounds from greedy and DSATUR.
| Bound | Value | Comes from |
|---|---|---|
| Lower | size of the largest clique ω(G) | all clique vertices need different colours |
| Upper | Δ + 1 | greedy in any order |
| Upper | degeneracy + 1 | greedy in smallest-last order |
| Exactly 2 | G has an edge and no odd cycle | BFS |
1static boolean tryColour(int v, int k, int[] colour, List<Integer>[] adj, int used) {2if (v == colour.length) return true;3for (int c = 0; c < k && c <= used; c++) { // c <= used: never open a colour before the one below it4boolean ok = true;5for (int u : adj[v]) if (u < v && colour[u] == c) ok = false;6if (!ok) continue;7colour[v] = c;8if (tryColour(v + 1, k, colour, adj, Math.max(used, c + 1))) return true;9}10colour[v] = -1;11return false;12}13static int chromaticNumber(int n, List<Integer>[] adj) { // smallest k that admits a proper k-colouring14for (int k = 1; k <= n; k++) {15int[] colour = new int[n];16if (tryColour(0, k, colour, adj, 0)) return k;17}18return n;19}
Worst
O(n · k^n) in the worst case
Space
O(n)
Graph colouring: 2-colouring, greedy, DSATUR and the exact chromatic number
Interactive · edit the inputundirected graph · 6 vertices · 6 edges
colour of each vertex
Greedy colouring: take the vertices in index order and give each the smallest colour that none of its already coloured neighbours uses. It never needs more than (maximum degree + 1) colours, but the order decides how many it really uses.
1@SuppressWarnings("unchecked")2static List<Integer>[] adjacency(int n, int[][] edges) {3List<Integer>[] adj = new List[n];4for (int i = 0; i < n; i++) adj[i] = new ArrayList<>();5for (int[] e : edges) { adj[e[0]].add(e[1]); adj[e[1]].add(e[0]); }6return adj;7}8static int[] bipartiteColouring(int n, List<Integer>[] adj) { // 2-colouring by BFS, or null when an odd cycle exists9int[] colour = new int[n];10Arrays.fill(colour, -1);11for (int s = 0; s < n; s++) {12if (colour[s] != -1) continue;13colour[s] = 0;14Deque<Integer> queue = new ArrayDeque<>(List.of(s));15while (!queue.isEmpty()) {16int u = queue.poll();17for (int v : adj[u]) {18if (colour[v] == colour[u]) return null; // both ends of an edge got the same colour19if (colour[v] == -1) { colour[v] = 1 - colour[u]; queue.add(v); }20}21}22}23return colour;24}25static int[] greedyColouring(int n, List<Integer>[] adj) { // vertices in index order, smallest free colour26int[] colour = new int[n];27Arrays.fill(colour, -1);28for (int v = 0; v < n; v++) {29boolean[] taken = new boolean[n + 1];30for (int u : adj[v]) if (colour[u] != -1) taken[colour[u]] = true;31int c = 0;32while (taken[c]) c++;33colour[v] = c;34}35return colour;36}37static int[] dsatur(int n, List<Integer>[] adj) { // always colour the most constrained vertex next38int[] colour = new int[n];39Arrays.fill(colour, -1);40for (int step = 0; step < n; step++) {41int pick = -1, bestSat = -1;42for (int v = 0; v < n; v++) {43if (colour[v] != -1) continue;44Set<Integer> seen = new HashSet<>(); // saturation: distinct colours among neighbours45for (int u : adj[v]) if (colour[u] != -1) seen.add(colour[u]);46if (seen.size() > bestSat || (seen.size() == bestSat && adj[v].size() > adj[pick].size())) { pick = v; bestSat = seen.size(); }47}48boolean[] taken = new boolean[n + 1];49for (int u : adj[pick]) if (colour[u] != -1) taken[colour[u]] = true;50int c = 0;51while (taken[c]) c++;52colour[pick] = c;53}54return colour;55}56static boolean tryColour(int v, int k, int[] colour, List<Integer>[] adj, int used) {57if (v == colour.length) return true;58for (int c = 0; c < k && c <= used; c++) { // c <= used: never open a colour before the one below it59boolean ok = true;60for (int u : adj[v]) if (u < v && colour[u] == c) ok = false;61if (!ok) continue;62colour[v] = c;63if (tryColour(v + 1, k, colour, adj, Math.max(used, c + 1))) return true;64}65colour[v] = -1;66return false;67}68static int chromaticNumber(int n, List<Integer>[] adj) { // smallest k that admits a proper k-colouring69for (int k = 1; k <= n; k++) {70int[] colour = new int[n];71if (tryColour(0, k, colour, adj, 0)) return k;72}73return n;74}
Live state
n
6
edges
6
Execution trace (dry run table)Show 8 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L25 | n=6 edges=6 | Greedy colouring: take the vertices in index order and give each the smallest colour that none of its already coloured neighbours uses. It never needs more than (maximum degree + 1) colours, but the order decides how many it really uses. |
| 2 | L33 | vertex=0 colour=0 colours_used=1 | Vertex 0: neighbours already use no colour, so the smallest free colour is 0. |
| 3 | L33 | vertex=1 colour=0 colours_used=1 | Vertex 1: neighbours already use no colour, so the smallest free colour is 0. |
| 4 | L33 | vertex=2 colour=1 colours_used=2 | Vertex 2: neighbours already use {0}, so the smallest free colour is 1. |
| 5 | L33 | vertex=3 colour=1 colours_used=2 | Vertex 3: neighbours already use {0}, so the smallest free colour is 1. |
| 6 | L33 | vertex=4 colour=2 colours_used=3 | Vertex 4: neighbours already use {0,1}, so the smallest free colour is 2. |
| 7 | L33 | vertex=5 colour=2 colours_used=3 | Vertex 5: neighbours already use {0,1}, so the smallest free colour is 2. |
| 8 | L35 | return=3 colouring=0:0 1:0 2:1 3:1 4:2 5:2 proper=yes | 3 colours used (greedy): 0:0 1:0 2:1 3:1 4:2 5:2. The colouring is proper, but it is only an upper bound on the chromatic number. |
DSATUR heuristic
**DSATUR** (degree of saturation) is a greedy colouring with a smarter order. The **saturation** of an uncoloured vertex is the number of different colours among its coloured neighbours. At each step DSATUR picks the uncoloured vertex with the highest saturation (ties go to the vertex with more neighbours) and gives it the smallest free colour. The most constrained vertex is coloured first, while it still has choices.
DSATUR is exact on bipartite graphs, cycles and wheels, and it usually beats plain greedy by a wide margin. In the animation, the second preset runs it on the crown graph: it needs only 2 colours, where index-order greedy needed 3 (the colouring is 0:0 1:1 2:0 3:1 4:0 5:1).
The simple implementation below is O(V^2 + V·E); a heap or bucket structure makes it faster. For exam timetabling and register allocation, DSATUR is the usual starting point, often followed by local search or an exact solver for the hardest part.
1static int[] dsatur(int n, List<Integer>[] adj) { // always colour the most constrained vertex next2int[] colour = new int[n];3Arrays.fill(colour, -1);4for (int step = 0; step < n; step++) {5int pick = -1, bestSat = -1;6for (int v = 0; v < n; v++) {7if (colour[v] != -1) continue;8Set<Integer> seen = new HashSet<>(); // saturation: distinct colours among neighbours9for (int u : adj[v]) if (colour[u] != -1) seen.add(colour[u]);10if (seen.size() > bestSat || (seen.size() == bestSat && adj[v].size() > adj[pick].size())) { pick = v; bestSat = seen.size(); }11}12boolean[] taken = new boolean[n + 1];13for (int u : adj[pick]) if (colour[u] != -1) taken[colour[u]] = true;14int c = 0;15while (taken[c]) c++;16colour[pick] = c;17}18return colour;19}
Worst
O(V² + V · E) with this simple scan
Space
O(V)
Graph colouring: 2-colouring, greedy, DSATUR and the exact chromatic number
Interactive · edit the inputundirected graph · 6 vertices · 6 edges
colour of each vertex
Greedy colouring: take the vertices in index order and give each the smallest colour that none of its already coloured neighbours uses. It never needs more than (maximum degree + 1) colours, but the order decides how many it really uses.
1@SuppressWarnings("unchecked")2static List<Integer>[] adjacency(int n, int[][] edges) {3List<Integer>[] adj = new List[n];4for (int i = 0; i < n; i++) adj[i] = new ArrayList<>();5for (int[] e : edges) { adj[e[0]].add(e[1]); adj[e[1]].add(e[0]); }6return adj;7}8static int[] bipartiteColouring(int n, List<Integer>[] adj) { // 2-colouring by BFS, or null when an odd cycle exists9int[] colour = new int[n];10Arrays.fill(colour, -1);11for (int s = 0; s < n; s++) {12if (colour[s] != -1) continue;13colour[s] = 0;14Deque<Integer> queue = new ArrayDeque<>(List.of(s));15while (!queue.isEmpty()) {16int u = queue.poll();17for (int v : adj[u]) {18if (colour[v] == colour[u]) return null; // both ends of an edge got the same colour19if (colour[v] == -1) { colour[v] = 1 - colour[u]; queue.add(v); }20}21}22}23return colour;24}25static int[] greedyColouring(int n, List<Integer>[] adj) { // vertices in index order, smallest free colour26int[] colour = new int[n];27Arrays.fill(colour, -1);28for (int v = 0; v < n; v++) {29boolean[] taken = new boolean[n + 1];30for (int u : adj[v]) if (colour[u] != -1) taken[colour[u]] = true;31int c = 0;32while (taken[c]) c++;33colour[v] = c;34}35return colour;36}37static int[] dsatur(int n, List<Integer>[] adj) { // always colour the most constrained vertex next38int[] colour = new int[n];39Arrays.fill(colour, -1);40for (int step = 0; step < n; step++) {41int pick = -1, bestSat = -1;42for (int v = 0; v < n; v++) {43if (colour[v] != -1) continue;44Set<Integer> seen = new HashSet<>(); // saturation: distinct colours among neighbours45for (int u : adj[v]) if (colour[u] != -1) seen.add(colour[u]);46if (seen.size() > bestSat || (seen.size() == bestSat && adj[v].size() > adj[pick].size())) { pick = v; bestSat = seen.size(); }47}48boolean[] taken = new boolean[n + 1];49for (int u : adj[pick]) if (colour[u] != -1) taken[colour[u]] = true;50int c = 0;51while (taken[c]) c++;52colour[pick] = c;53}54return colour;55}56static boolean tryColour(int v, int k, int[] colour, List<Integer>[] adj, int used) {57if (v == colour.length) return true;58for (int c = 0; c < k && c <= used; c++) { // c <= used: never open a colour before the one below it59boolean ok = true;60for (int u : adj[v]) if (u < v && colour[u] == c) ok = false;61if (!ok) continue;62colour[v] = c;63if (tryColour(v + 1, k, colour, adj, Math.max(used, c + 1))) return true;64}65colour[v] = -1;66return false;67}68static int chromaticNumber(int n, List<Integer>[] adj) { // smallest k that admits a proper k-colouring69for (int k = 1; k <= n; k++) {70int[] colour = new int[n];71if (tryColour(0, k, colour, adj, 0)) return k;72}73return n;74}
Live state
n
6
edges
6
Execution trace (dry run table)Show 8 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L25 | n=6 edges=6 | Greedy colouring: take the vertices in index order and give each the smallest colour that none of its already coloured neighbours uses. It never needs more than (maximum degree + 1) colours, but the order decides how many it really uses. |
| 2 | L33 | vertex=0 colour=0 colours_used=1 | Vertex 0: neighbours already use no colour, so the smallest free colour is 0. |
| 3 | L33 | vertex=1 colour=0 colours_used=1 | Vertex 1: neighbours already use no colour, so the smallest free colour is 0. |
| 4 | L33 | vertex=2 colour=1 colours_used=2 | Vertex 2: neighbours already use {0}, so the smallest free colour is 1. |
| 5 | L33 | vertex=3 colour=1 colours_used=2 | Vertex 3: neighbours already use {0}, so the smallest free colour is 1. |
| 6 | L33 | vertex=4 colour=2 colours_used=3 | Vertex 4: neighbours already use {0,1}, so the smallest free colour is 2. |
| 7 | L33 | vertex=5 colour=2 colours_used=3 | Vertex 5: neighbours already use {0,1}, so the smallest free colour is 2. |
| 8 | L35 | return=3 colouring=0:0 1:0 2:1 3:1 4:2 5:2 proper=yes | 3 colours used (greedy): 0:0 1:0 2:1 3:1 4:2 5:2. The colouring is proper, but it is only an upper bound on the chromatic number. |
Coloring complexity
Deciding whether a graph can be coloured with k colours is **NP-complete for every fixed k ≥ 3**, while k = 2 is the easy bipartite test and k = 1 is “no edges”. The optimisation problem is also hard to approximate: unless P = NP, no polynomial algorithm approximates χ(G) within n^(1 − ε).
There are bright spots. Interval graphs (and chordal graphs) are coloured optimally by a greedy rule in a perfect elimination order. Planar graphs can always be 4-coloured (the Four Colour Theorem, proved with computer assistance in 1976 and 1997) and 5-colouring a planar graph is easy and constructive. Trees need 2 colours; bounded-treewidth graphs are solved by dynamic programming.
In practice a mix is used: bounds from cliques and greedy, DSATUR for a good colouring, then exact search (SAT or DSATUR-based branch and bound) when the instance is small enough. The next lesson on planar graphs continues with the Four Colour Theorem's setting.
| Graph class | Colouring |
|---|---|
| Bipartite | 2 colours, BFS |
| Trees and forests | 2 colours |
| Interval / chordal | optimal greedy in the right order |
| Planar | ≤ 4 always, ≤ 5 constructively, ≤ 6 by greedy |
| General | NP-hard to decide for k ≥ 3, hard to approximate |
Practice ladder
Now try it yourself
Easy
- Is Graph Bipartite?
Medium
- Possible Bipartition
- Flower Planting With No Adjacent
Advanced
- Maximum Students Taking Exam
Key takeaways
- 2-colourable = bipartite = no odd cycle, tested by BFS in O(V + E).
- Greedy needs at most max degree + 1 colours but depends on the vertex order; DSATUR picks the most constrained vertex first.
- The exact chromatic number is NP-hard; backtracking with symmetry breaking handles small graphs.