Bipartite Check and Graph Coloring — 2-Coloring with BFS or DFS [LC 785, LC 886, Google, Meta]
Advertisement
Problem Statement
Given an undirected graph with
nvertices, determine if it is bipartite — that is, can the vertices be split into two disjoint setsAandBsuch that every edge connects a vertex inAwith a vertex inB? Equivalently, can the graph be 2-colored such that no two adjacent vertices share a color?
Constraints:
1 <= n <= 10^50 <= m <= 10^5- The graph may be disconnected; multiple components must each be checked.
Example 1:
Input: graph = [[1,3],[0,2],[1,3],[0,2]]
Output: true
Explanation: Color {0, 2} red and {1, 3} blue. Every edge crosses colors.Example 2 (odd cycle):
Input: graph = [[1,2,3],[0,2],[0,1,3],[0,2]]
Output: false
Explanation: 0 -- 1 -- 2 -- 0 forms a triangle (odd cycle), which cannot be 2-colored.Why This Problem Matters
Bipartite graphs appear everywhere: matching engineers to projects, students to courses, advertisers to slots, GPUs to jobs. Whenever an interview problem mentions "two groups," "matching," or "compatibility," check first if the underlying graph is bipartite — many otherwise-hard problems become polynomial-time on bipartite graphs (maximum matching is one famous example).
LeetCode 785 (Is Graph Bipartite?) and 886 (Possible Bipartition) are the entry-level FAANG problems that test your ability to 2-color a graph in linear time. Bipartite checking also serves as a stepping stone to more advanced techniques: maximum bipartite matching (Hopcroft-Karp), Konig's theorem, vertex covers, and the famous 2-SAT reduction.
In production systems, bipartite tests show up in auction matching at Google, content-creator-to-advertiser allocation at Meta, and shipper-to-driver matching at Uber. Recognising bipartiteness is the gateway to applying matching algorithms that solve real-world allocation problems efficiently.
The Core Insight
A graph is bipartite if and only if it has no odd-length cycle. The 2-coloring algorithm constructs a proof of bipartiteness or a witness odd cycle in linear time.
BFS approach: Pick any unvisited vertex, color it 0. BFS-expand, alternating colors layer by layer. If you ever encounter an already-colored neighbour with the same color as the current vertex, the graph contains an odd cycle and is not bipartite. Otherwise, the BFS completes and produces a valid 2-coloring.
DFS approach: Color a starting vertex 0. Recurse: for each neighbour, if uncolored, color it the opposite color and recurse; if colored the same as the current vertex, return false. The recursion bottom-out gives a valid coloring.
Both approaches run in O(V + E) and require O(V) space. BFS is often easier to write iteratively (no recursion limit concerns); DFS gives shorter code.
For disconnected graphs, loop over every vertex and start a fresh BFS/DFS for each unvisited one. Each component is independently bipartite or not.
Visual Dry Run
graph = [[1,2,3],[0,2],[0,1,3],[0,2]]. Start BFS from vertex 0.
| Step | Queue | Color | Action |
|---|---|---|---|
| 1 | [0] | 0:0 | initial: color 0 = 0 |
| 2 | [1, 2, 3] | 0:0, 1:1, 2:1, 3:1 | all neighbours of 0 colored 1 |
| 3 | [2, 3] | same | pop 1; neighbour 2 already colored 1 (same as 1) -> return false |
The triangle 0-1-2-0 is detected when 1's neighbour 2 already shares 1's color.
Solution (Optimal)
Python
from collections import deque
def is_bipartite_bfs(graph):
n = len(graph)
color = [-1] * n # -1 means uncolored
for start in range(n):
if color[start] != -1:
continue # already explored
color[start] = 0
queue = deque([start])
while queue:
u = queue.popleft()
for v in graph[u]:
if color[v] == -1:
color[v] = 1 - color[u] # flip color
queue.append(v)
elif color[v] == color[u]:
return False # odd cycle witnessed
return True
def is_bipartite_dfs(graph):
n = len(graph)
color = [-1] * n
def dfs(u, c):
color[u] = c
for v in graph[u]:
if color[v] == -1:
if not dfs(v, 1 - c):
return False
elif color[v] == c:
return False
return True
for i in range(n):
if color[i] == -1 and not dfs(i, 0):
return False
return TrueJavaScript
function isBipartite(graph) {
const n = graph.length;
const color = new Array(n).fill(-1);
for (let start = 0; start < n; start++) {
if (color[start] !== -1) continue;
color[start] = 0;
const queue = [start];
let head = 0;
while (head < queue.length) {
const u = queue[head++];
for (const v of graph[u]) {
if (color[v] === -1) {
color[v] = 1 - color[u];
queue.push(v);
} else if (color[v] === color[u]) {
return false;
}
}
}
}
return true;
}
function possibleBipartition(n, dislikes) {
// LC 886: convert to graph and run isBipartite. People are 1-indexed.
const graph = Array.from({ length: n + 1 }, () => []);
for (const [a, b] of dislikes) { graph[a].push(b); graph[b].push(a); }
const color = new Array(n + 1).fill(-1);
function dfs(u, c) {
color[u] = c;
for (const v of graph[u]) {
if (color[v] === -1) {
if (!dfs(v, 1 - c)) return false;
} else if (color[v] === c) {
return false;
}
}
return true;
}
for (let i = 1; i <= n; i++) {
if (color[i] === -1 && !dfs(i, 0)) return false;
}
return true;
}Complexity: Time O(V + E). Space O(V) for the color array plus O(V) for queue or recursion stack.
Common Mistakes
- Not handling disconnected graphs. A single BFS may not visit every vertex. Loop over all vertices and start fresh when uncolored.
- Mixing color values. Use 0 and 1 (or 1 and 2). Avoid 0 vs
nullbecausenullinterferes with the "uncolored" sentinel. - Returning early on the first colored neighbour without a same-color check. Only conflict cases should fail.
- Confusing directed and undirected graphs. Bipartiteness is defined for undirected graphs. Directed bipartiteness needs different definitions.
- Off-by-one in 1-indexed problems. LC 886 uses 1-indexed people; allocate
n + 1slots. - Recursion depth on large graphs. Use iterative BFS or raise the recursion limit for
nnear 10^5.
Interview Tips
- Open by stating the equivalence: "A graph is bipartite iff it has no odd cycle." This shows theoretical understanding.
- Mention both BFS and DFS implementations; offer the one the interviewer prefers.
- For disconnected graphs, explicitly mention the outer loop.
- For LC 886, walk through the reduction: dislikes become edges; the question is whether the dislike graph is bipartite.
- If asked about k-coloring (k > 2), state honestly that this is NP-complete in general but admits known polynomial-time algorithms for special graph classes (planar, bipartite).
Follow-up Questions
- Maximum bipartite matching: Use Hopcroft-Karp algorithm in
O(E * sqrt(V)). - Konig's theorem: In bipartite graphs, max matching equals min vertex cover.
- 2-SAT solver: Build implication graph, run Tarjan SCC, ensure variable and its negation lie in different SCCs.
- What if the graph has self-loops? A self-loop is a 1-cycle (odd). The graph is automatically not bipartite.
- LC 886 (Possible Bipartition): Direct application — bipartite check on the dislike graph.
Key Takeaways
- A graph is bipartite if and only if it contains no odd-length cycle.
- BFS and DFS both 2-color the graph in
O(V + E)time. - For disconnected graphs, run the algorithm separately for each component.
- A conflict (same-color neighbour) witnesses an odd cycle and proves non-bipartiteness.
- Bipartite graphs unlock polynomial-time matching, vertex cover, and 2-SAT algorithms.
- LeetCode 785 and 886 are the standard FAANG interview problems that test the bipartite-check pattern in a single linear pass.
Advertisement