Topics › Graphs › Problems on BFS/DFS
Bipartite Graph (DFS)
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
You are given an undirected graph with V vertices as an adjacency list. The graph is bipartite if its vertices can be split into two groups so that every edge joins a vertex of one group to a vertex of the other; no edge stays inside a group. Equivalently, you can colour every vertex with one of two colours so that the two ends of every edge have different colours. Return true if the graph is bipartite.
The graph may be disconnected, and every component must be colourable. Constraints: 1 ≤ V ≤ 10^5, 0 ≤ E ≤ 10^5.
Example 1 (LeetCode 785, graph = [[1,2,3],[0,2],[0,1,3],[0,2]])
0 --- 1
| \ |
| \ |
3 --- 2 edges: 0-1, 0-2, 0-3, 1-2, 2-3
answer = false // 0, 1, 2 form a triangle: three vertices, two colours, two must clash
Example 2 (graph = [[1,3],[0,2],[1,3],[0,2]])
0 --- 1
| |
3 --- 2 edges: 0-1, 1-2, 2-3, 3-0
answer = true // groups {0, 2} and {1, 3}
The classical characterisation: a graph is bipartite exactly when it has no cycle of odd length. Walking around a cycle, the colours must alternate, so the cycle can only close if its length is even. Example 1 fails because of the triangle 0-1-2. The algorithms below never search for odd cycles explicitly; they try to colour and detect the clash that an odd cycle forces.
Approach 1: Brute force, try every colouring
Intuition. A colouring assigns one bit to each vertex, so there are 2^V colourings. Check each one against every edge, and answer true if any of them has no clash. The integer mask encodes a colouring: bit u is the colour of vertex u.
Algorithm.
- For each
maskfrom0to2^V - 1: - For every edge
u - v, compare bituand bitvofmask; on equality, this colouring fails. - If some colouring passes every edge, return
true; otherwisefalse.
static boolean bruteForce(int V, List<List<Integer>> adj) {
for (int mask = 0; mask < (1 << V); mask++) { // bit u of mask = colour of vertex u
boolean ok = true;
for (int u = 0; u < V && ok; u++) {
for (int v : adj.get(u)) {
if (((mask >> u) & 1) == ((mask >> v) & 1)) { ok = false; break; }
}
}
if (ok) return true;
}
return false;
}
Complexity. Time O(2^V · (V + E)). Space O(1) extra. It only works for about V ≤ 20, and it wastes effort: once the colour of one vertex is chosen, the colours of its whole component are forced, so almost all of these colourings are pointless to try.
Approach 2: Optimal, DFS colouring
Intuition. Pick any uncoloured vertex and give it colour 0. Every neighbour must then have colour 1, every neighbour of those must have colour 0, and so on. Nothing is being guessed: inside a connected component, the colour of the first vertex decides all the others. The first choice is also harmless, because swapping the two colours of a valid colouring gives another valid colouring.
So DFS simply propagates colours. When it meets a neighbour that is already coloured, there are two possibilities:
- the neighbour has the opposite colour, which is consistent, so move on;
- the neighbour has the same colour as the current vertex, so the forced colouring breaks, and since it was forced, no other colouring of this component can work either. Return
false.
Why does a clash really mean an odd cycle? The colour of each vertex equals the parity of its depth in the DFS tree. Two same-coloured vertices u and v joined by an edge have depths of equal parity, so the tree paths from them to their lowest common ancestor have lengths of the same parity, and together with the edge u - v they form a cycle of odd length.
Algorithm.
- Set every
colorto-1(uncoloured). - For each uncoloured vertex
s, callpaint(s, 0); returnfalseif it fails. The loop is what handles disconnected graphs. paint(u, c): setcolor[u] = c. For each neighbourv: if uncoloured,paint(v, 1 - c)and propagate failure; ifcolor[v] == c, fail.
static boolean dfs(int V, List<List<Integer>> adj) {
int[] color = new int[V];
Arrays.fill(color, -1); // -1 = not coloured yet
for (int s = 0; s < V; s++) {
if (color[s] == -1 && !paint(s, 0, adj, color)) return false; // each component starts fresh
}
return true;
}
static boolean paint(int u, int c, List<List<Integer>> adj, int[] color) {
color[u] = c;
for (int v : adj.get(u)) {
if (color[v] == -1) {
if (!paint(v, 1 - c, adj, color)) return false;
} else if (color[v] == c) {
return false; // both ends of edge u-v have the same colour
}
}
return true;
}
Dry run on Example 1 (adjacency 0: [1,2,3], 1: [0,2], 2: [0,1,3]):
| call | neighbour | its colour | result |
|---|---|---|---|
| paint(0, 0) | 1 | none | paint(1, 1) |
| paint(1, 1) | 0 | 0 | fine, differs from 1 |
| paint(1, 1) | 2 | none | paint(2, 0) |
| paint(2, 0) | 0 | 0 | same as 2’s colour: return false |
The clash on edge 2 - 0 closes the odd cycle 0 - 1 - 2 - 0.
Complexity. Time O(V + E): every vertex is coloured once and every adjacency list scanned once. Space O(V) for color and the recursion stack.
Approach 3: BFS colouring
Intuition. The propagation does not care about order, only that each newly reached vertex gets the opposite colour of the vertex it was reached from. BFS does the same thing layer by layer: layer k of the BFS gets colour k mod 2. A clash is an edge inside one layer, which again closes an odd cycle. This version avoids deep recursion, which matters in Java when V is large and the graph is a long path.
static boolean bfs(int V, List<List<Integer>> adj) {
int[] color = new int[V];
Arrays.fill(color, -1);
for (int s = 0; s < V; s++) {
if (color[s] != -1) continue;
color[s] = 0;
Deque<Integer> q = new ArrayDeque<>();
q.offer(s);
while (!q.isEmpty()) {
int u = q.poll();
for (int v : adj.get(u)) {
if (color[v] == -1) { color[v] = 1 - color[u]; q.offer(v); }
else if (color[v] == color[u]) return false;
}
}
}
return true;
}
Complexity. Time O(V + E), space O(V) for the queue and colours.
Edge cases and pitfalls
- Disconnected graphs. Colouring only from vertex
0misses other components. The test with a path plus a separate triangle must returnfalse. - Isolated vertices and empty graphs are bipartite; every method returns
true. - Self-loops. An edge
u - ucan never have two different colours;paintcatches it becausecolor[u] == cwhen it looks at itself. - Trees and forests have no cycles at all, so they are always bipartite.
- Using a boolean visited array. It is not enough to know that a neighbour was visited; you must know which colour it got. The
colorarray with-1for “unvisited” carries both pieces of information. - Input format. LeetCode gives
graph[u]as an array of neighbours with both directions present; converting it toList<List<Integer>>changes nothing in the logic.
Related problems
- Bipartite graph is the BFS formulation of the same check.
- Bipartite Check using DFS is the SDE sheet entry for this DFS version.
- Detect a cycle in an undirected graph uses the same DFS skeleton to find any cycle, odd or even.
- Cycle Detection in Directed Graph (DFS) adapts DFS state tracking to directed edges.