Vertex colouring is easy for (bipartite) and NP-complete for , even on planar graphs. Exact chromatic number for is by inclusion–exclusion; the chromatic polynomial for is ; structured classes (chordal, interval, perfect) are polynomial (Special Graphs). Edge colouring needs colours on bipartite graphs (König, constructive) and or on simple graphs (Vizing, constructive ).
Bounds
| Bound | Statement |
|---|---|
| clique | |
| independence | |
| greedy | in any order |
| degeneracy | , = max over subgraphs of the min degree (colour in reverse smallest-last order) |
| Brooks | unless is a complete graph or an odd cycle |
| planar | (5 constructively in linear time) |
| perfect graphs |
Proof: greedy and degeneracy bounds
Greedy gives each vertex the smallest colour not used by its already-coloured neighbours; it has at most of them, so some colour in is free. For degeneracy, repeatedly remove a minimum-degree vertex (degree in the remaining graph) and colour in reverse removal order: when a vertex is coloured, its coloured neighbours are those removed after it, at most of them. because each colour class is an independent set of size .
Heuristics: Welsh–Powell (sort vertices by degree descending once, then greedy-colour in that fixed order) is the simplest named heuristic and already realises the degeneracy-style bound in practice on many graphs, though it can be beaten by adversarial orderings since the order is static. DSATUR (colour next the vertex with the most distinct colours among its neighbours, ties by degree) re-picks the order dynamically as colours are assigned, is exact on bipartite graphs, and is strictly stronger in practice; combined with a clique lower bound and backtracking it solves sparse instances.
Complexity by class: deciding is (BFS bipartiteness check); deciding is NP-complete even restricted to planar graphs — the same 2-vs-3 cliff as 2-SAT vs 3-SAT; computing exactly is NP-hard and inapproximable within for any unless P = NP; (or 1 if edgeless); exactly on interval and chordal graphs (greedy by left endpoint / perfect elimination order respectively).
Interval graphs are the class that appears most in contests, almost always disguised as “minimum number of rooms/machines/platforms” rather than named as colouring — recognise it and the problem collapses to maximum simultaneous overlap, swept in (Interval Problems).
Exact chromatic number in
is -colourable iff the number of ordered -tuples of independent sets covering is nonzero, and by inclusion–exclusion that number is , where = number of independent subsets of (including ).
int chromaticNumber(const vector<int>& adj) { // adj[v] = neighbour bitmask, n <= ~23
int n = sz(adj), N = 1 << n; if (!n) return 0;
const ull P = (1ULL << 61) - 1; // exact count mod a large prime; false "0" is negligible
auto mulmod = [&](ull a, ull b) { __uint128_t c = (__uint128_t)a * b; ull r = (ull)(c & P) + (ull)(c >> 61); return r >= P ? r - P : r; };
vector<ull> I(N), pw(N, 1); I[0] = 1;
for (int S = 1; S < N; S++) { int v = __builtin_ctz(S); I[S] = I[S ^ 1 << v] + I[(S ^ 1 << v) & ~adj[v]]; }
for (int k = 1; k <= n; k++) {
ull tot = 0;
for (int S = 0; S < N; S++) {
pw[S] = mulmod(pw[S], I[S] % P);
tot = ((n - __builtin_popcount(S)) & 1) ? (tot + P - pw[S]) % P : (tot + pw[S]) % P;
}
if (tot) return k;
}
return n;
}Proof: the inclusion–exclusion count and the recurrence for
counts -tuples of independent sets all contained in . A tuple whose union is exactly is counted for every with total sign . So the alternating sum counts tuples covering ; such a tuple exists iff splits into independent sets (take differences of the sets). : independent subsets of either avoid or contain and none of its neighbours.
| Method | Time | Practical |
|---|---|---|
| inclusion–exclusion above | 20–25, the best known exact asymptotic | |
bitmask DP: dp[mask] = min colours to cover mask, transitioning over maximal independent submasks | , or with [[06-bit-manipulation/03-submasks-and-sos-dp | SOS]] preprocessing of which subsets are independent |
| try with DSATUR + clique lower bound + backtracking | exponential, no clean bound | 100+ on sparse instances |
Chromatic polynomial
= number of proper colourings with at most colours. Deletion–contraction: . Closed forms: trees , cycles , : , chordal graphs . counts acyclic orientations (Stanley). For general graphs with : where = number of ordered partitions of into nonempty independent sets, computed by a ranked (subset-convolution style) inclusion–exclusion in .
struct ChromaticPolynomial { // mint = modular integer type
int n; vector<mint> E; // E[j]: ordered covers by j disjoint nonempty independent sets
ChromaticPolynomial(const vector<int>& g) : n(sz(g)), E(n + 1) {
int full = 1 << n; vector<char> indep(full, 1);
for (int s = 1; s < full; s++) { int v = __builtin_ctz(s), r = s & (s - 1); indep[s] = indep[r] && !(g[v] & r); }
vector<vector<mint>> f(n + 1, vector<mint>(full)); // f[r][T] = # independent r-subsets of T
for (int s = 0; s < full; s++) if (indep[s]) f[__builtin_popcount(s)][s] = 1;
for (int r = 0; r <= n; r++) for (int i = 0; i < n; i++) for (int s = 0; s < full; s++) if (s >> i & 1) f[r][s] += f[r][s ^ 1 << i];
vector<vector<mint>> q(n + 1);
for (int s = 0; s < full; s++) { // [y^j] q_n = [z^n] (sum_{r>=1} f[r][s] z^r)^j
int t = __builtin_popcount(s); q[0] = {mint(1)};
for (int m = 1; m <= n; m++) {
q[m].assign(m + 1, mint(0));
for (int r = 1; r <= min(m, t); r++) { mint a = f[r][s]; if (a == mint(0)) continue; for (int j = 0; j < sz(q[m - r]); j++) q[m][j + 1] += a * q[m - r][j]; }
}
for (int j = 0; j <= n; j++) { mint e = j < sz(q[n]) ? q[n][j] : mint(0); if ((n - t) & 1) E[j] -= e; else E[j] += e; }
}
}
mint operator()(ll x) const { mint res = 0, b = 1; for (int j = 0; j <= n; j++) { res += E[j] * b; b = b * (mint(x) - mint(j)) / mint(j + 1); } return res; }
};Proof sketch: the ranked inclusion–exclusion
A proper colouring with at most colours is an assignment of the vertex set to an ordered sequence of nonempty colour classes chosen from colours: . counts ordered -tuples of disjoint nonempty independent sets with union ; disjointness plus covering is equivalent to “sizes sum to and the union is ”. Inclusion–exclusion over the allowed ground set removes the union condition, and tracking total size by the variable enforces disjointness (size sum with union inside and union forces a partition). For fixed , over tuples of , and the DP extracts of all powers at once.
Edge colouring
Bipartite (König): exactly colours. Simple constructive method in : for each edge take a colour free at and free at ; if , flip the alternating path starting at (it cannot reach in a bipartite graph), then colour with .
vector<int> bipartiteEdgeColoring(int L, int R, const vector<pair<int, int>>& E) { // multigraph; colours in [0, Δ)
int n = L + R, D = 0; vector<int> deg(n);
for (auto [a, b] : E) D = max({D, ++deg[a], ++deg[L + b]});
vector<vector<int>> at(n, vector<int>(D, -1)); // at[v][c] = edge id with colour c at v
vector<int> col(sz(E), -1);
auto other = [&](int e, int v) { return v == E[e].first ? L + E[e].second : E[e].first; };
for (int e = 0; e < sz(E); e++) {
int u = E[e].first, v = L + E[e].second, a = 0, b = 0;
while (at[u][a] >= 0) a++;
while (at[v][b] >= 0) b++;
if (a != b) { // flip the a/b path starting at v
vector<int> path; int x = v, c = a;
while (at[x][c] >= 0) { int f = at[x][c]; path.push_back(f); x = other(f, x); c ^= a ^ b; }
for (int f : path) { int p = E[f].first, q = L + E[f].second; at[p][col[f]] = at[q][col[f]] = -1; }
for (int f : path) { col[f] ^= a ^ b; int p = E[f].first, q = L + E[f].second; at[p][col[f]] = at[q][col[f]] = f; }
}
col[e] = a; at[u][a] = at[v][a] = e;
}
return col;
}For up to – (Library Checker “Bipartite Edge Coloring”), make the graph -regular by merging low-degree vertices and adding dummy edges, then recursively split: an even-degree regular bipartite graph splits into two halves by alternating colours along Euler circuits (Euler Tours); an odd degree peels off one perfect matching (Hopcroft–Karp) first: about in practice.
Simple graphs (Vizing): colours suffice, found in by fan rotations and alternating-path flips; deciding whether suffices is NP-hard.
vector<int> vizing(int n, const vector<pair<int, int>>& E) { // simple graph; colours in [0, Δ]
vector<int> cc(n + 1), res(sz(E)), fan(n), freeC(n), loc;
for (auto [u, v] : E) cc[u]++, cc[v]++;
int K = *max_element(all(cc)) + 1;
vector<vector<int>> adj(n, vector<int>(K, -1)); // adj[u][c] = neighbour joined by colour c
for (auto [u, v0] : E) {
int v = v0, at = u, end = u, d, c = freeC[u], ind = 0, i = 0;
fan[0] = v; loc.assign(K, 0);
while (d = freeC[v], !loc[d] && (v = adj[u][d]) != -1) loc[d] = ++ind, cc[ind] = d, fan[ind] = v;
cc[loc[d]] = c;
for (int cd = d; at != -1; cd ^= c ^ d, at = adj[at][cd]) swap(adj[at][cd], adj[at][cd ^ c ^ d]), end = at;
while (adj[fan[i]][d] != -1) { int l = fan[i], r = fan[++i], e = cc[i]; adj[u][e] = l; adj[l][e] = u; adj[r][e] = -1; freeC[r] = e; }
adj[u][d] = fan[i]; adj[fan[i]][d] = u;
for (int y : {fan[0], u, end}) { int& z = freeC[y] = 0; while (adj[y][z] != -1) z++; }
}
for (int i = 0; i < sz(E); i++) { auto [u, v] = E[i]; while (adj[u][res[i]] != v) res[i]++; }
return res;
}Proof: König's edge colouring theorem (the flip never hits )
Colour is free at , is free at , , and is used at . The maximal path starting at with colours is unique (each vertex has at most one edge of each colour). Since and are on opposite sides, along this path the edges coloured always go from ‘s side to ‘s side and the edges coloured go back; so the path can only enter ‘s side by an -edge, and it can’t enter because is free at . Swapping and on the path keeps the colouring proper (at the far end of the path the swapped-in colour was free, by maximality) and makes free at , so gets colour . With colours each endpoint always has a free colour before its edge is coloured, so colours suffice.
Vizing (): the fan argument — rotate the fan of neighbours of whose edges can shift colours, and if stuck, flip a alternating path from ; a counting argument with colours ensures one of these succeeds. The code implements this standard proof.
Variants worth naming
| Variant | Note |
|---|---|
| List colouring | each vertex has its own allowed colour set — strictly harder than ordinary colouring |
| Equitable colouring | colour classes must be nearly equal in size |
| Fractional chromatic number | the LP relaxation of colouring; polynomial for perfect graphs |
| Total colouring | colour vertices and edges together, no shared colour between adjacent/incident elements |
Where this shows up beyond contests: register allocation in compilers (the classic application), scheduling under pairwise conflicts (exams, meetings, frequency assignment), map colouring (planar, hence ), and Sudoku (a colouring of a specific 81-vertex graph with 9 colours).