Graph
2-color the graph using DFS or BFS. If a neighbor has the same color as the current node, the graph is not bipartite. Used for detecting odd cycles and validating team assignment constraints.
Imagine you are a teacher dividing students into two teams for a debate. Some pairs of students are rivals and refuse to be on the same team. Can you always find an assignment that keeps every pair of rivals on opposite teams? And if so, how do you find it efficiently?
This is the graph 2-coloring problem. Model each student as a node and each rivalry as an edge. You need to assign each node one of two colors, say 0 and 1, such that no edge connects two nodes of the same color. A graph that admits such a coloring is called bipartite. Every bipartite graph can be partitioned into two independent sets where all edges go between the sets, never within them.
The standard algorithm for checking bipartiteness uses BFS. Pick any uncolored node, assign it color 0, and place it in a queue. Then process the queue: for each node you dequeue, look at all its neighbors. If a neighbor has not been colored yet, assign it the opposite color (1 if the current node is 0, 0 if the current node is 1) and enqueue it. If a neighbor is already colored and its color matches the current node's color, you have found a conflict. Two adjacent nodes want the same color, which means the graph is not bipartite.
The expression 1 - color[node] is a clean way to flip between 0 and 1. If the current node has color 0, its neighbors get 1 - 0 = 1. If the current node has color 1, its neighbors get 1 - 1 = 0.
Consider a graph with 6 nodes and these edges: 0-1, 0-3, 1-2, 2-3, 3-4, 4-5.
Start BFS from node 0. Color it 0.
Process node 0 (color 0). Neighbors are 1 and 3. Neither is colored. Color both 1. Queue: [1, 3].
Process node 1 (color 1). Neighbor 0 is already color 0, which differs from 1, so no conflict. Neighbor 2 is uncolored. Color it 0. Queue: [3, 2].
Process node 3 (color 1). Neighbor 0 is color 0, fine. Neighbor 2 is color 0, fine. Neighbor 4 is uncolored. Color it 0. Queue: [2, 4].
Process node 2 (color 0). Neighbor 1 is color 1, fine. Neighbor 3 is color 1, fine. Queue: [4].
Process node 4 (color 0). Neighbor 3 is color 1, fine. Neighbor 5 is uncolored. Color it 1. Queue: [5].
Process node 5 (color 1). Neighbor 4 is color 0, fine. Queue is empty.
No conflicts were found. The graph is bipartite with partition {0, 2, 4} in color 0 and {1, 3, 5} in color 1. Every edge crosses between the two groups.
Now consider a triangle: nodes 0, 1, 2 with edges 0-1, 1-2, 0-2.
Start from node 0. Color it 0. Process node 0: color neighbors 1 and 2 as 1. Queue: [1, 2].
Process node 1 (color 1). Neighbor 0 is color 0, fine. Neighbor 2 is color 1, which is the same as node 1's color. Conflict. The graph is not bipartite.
The triangle is an odd cycle of length 3, and no 2-coloring can handle it. If you color 0 as 0, then 1 must be 1, and 2 must be 0 (because it neighbors 1). But 2 also neighbors 0, and both are color 0. There is no escape.
For LeetCode 785, the input graph is an adjacency list where graph[i] is the list of neighbors of node i, and you must determine whether the graph is bipartite.
from collections import deque
def is_bipartite(graph):
n = len(graph)
color = [-1] * n
for start in range(n):
if color[start] != -1:
continue
queue = deque([start])
color[start] = 0
while queue:
node = queue.popleft()
for nei in graph[node]:
if color[nei] == -1:
color[nei] = 1 - color[node]
queue.append(nei)
elif color[nei] == color[node]:
return False
return TrueThe color array starts as all -1, meaning uncolored. The outer loop ensures that every connected component is checked, which is critical for disconnected graphs. Within each component, BFS assigns alternating colors and watches for conflicts.
A graph is bipartite if and only if it contains no odd-length cycle. Here is the intuition.
In a 2-coloring, every edge flips the color. Walking along a path, the color alternates: 0, 1, 0, 1, 0, 1, and so on. After traversing an even number of edges, you return to the original color. After an odd number, you land on the opposite color.
A cycle brings you back to the starting node. If the cycle has even length, you return to the same color you started with, and there is no conflict. If the cycle has odd length, you return needing the opposite color, but the starting node already has its original color. That is a contradiction. No 2-coloring can satisfy all the edges in an odd cycle.
Even cycles are perfectly fine. A cycle of length 4, like 0-1-2-3-0, can be 2-colored as 0, 1, 0, 1. A cycle of length 6 works the same way. It is specifically odd cycles that cause trouble, and a single odd cycle anywhere in the graph is enough to make it non-bipartite.
The graph might not be connected. If you only BFS from node 0, you will miss any nodes in other connected components. Those components might have their own odd cycles that would make the graph non-bipartite.
The solution is the outer for-loop: iterate through all nodes 0 to n-1, and for each uncolored node, start a new BFS from it. This guarantees every component is examined. A graph is bipartite only if every one of its connected components is bipartite.
This problem gives you n people numbered 1 to n and a list of pairs who dislike each other. You need to determine whether you can split everyone into two groups such that no two people who dislike each other are in the same group.
This is exactly bipartite checking with different framing. Build a graph where each dislike pair is an edge, then check if the graph is bipartite. The people are the nodes, the dislikes are the edges, and the two groups are the two colors.
from collections import deque, defaultdict
def possible_bipartition(n, dislikes):
graph = defaultdict(list)
for u, v in dislikes:
graph[u].append(v)
graph[v].append(u)
color = [-1] * (n + 1) # 1-indexed
for start in range(1, n + 1):
if color[start] != -1:
continue
queue = deque([start])
color[start] = 0
while queue:
node = queue.popleft()
for nei in graph[node]:
if color[nei] == -1:
color[nei] = 1 - color[node]
queue.append(nei)
elif color[nei] == color[node]:
return False
return TrueThe only differences from the basic bipartite check are that the nodes are 1-indexed and the graph is built from the dislike pairs rather than given as an adjacency list. The core BFS logic is identical.
You can also check bipartiteness using Union-Find (Disjoint Set Union). The idea is based on a key property: in a bipartite graph, every neighbor of node u must be in the opposite group from u. Therefore, all neighbors of u must be in the same group as each other.
For each node u, union all of u's neighbors together. Then check: if u and any of its neighbors end up in the same set, the graph is not bipartite.
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
def find(self, x):
while self.parent[x] != x:
self.parent[x] = self.parent[self.parent[x]]
x = self.parent[x]
return x
def union(self, x, y):
px, py = self.find(x), self.find(y)
if px == py:
return
if self.rank[px] < self.rank[py]:
px, py = py, px
self.parent[py] = px
if self.rank[px] == self.rank[py]:
self.rank[px] += 1
def is_bipartite_uf(graph):
n = len(graph)
uf = UnionFind(n)
for node in range(n):
for nei in graph[node]:
if uf.find(node) == uf.find(nei):
return False
uf.union(graph[node][0], nei)
return TrueFor each node, we check whether it has already been merged with any of its neighbors (which would mean they ended up on the same side, a contradiction). If not, we union all neighbors together, enforcing that they share one side. This approach avoids explicit coloring and can be useful when Union-Find is already part of your solution for other reasons.
The BFS approach visits every node once and examines every edge once, giving time and space for the color array and queue.
The Union-Find approach also processes every edge once. With path compression and union by rank, each find and union operation is nearly amortized (technically , where alpha is the inverse Ackermann function). So the total time is , which is effectively in practice. Space is for the parent and rank arrays.
Both approaches are optimal since you must inspect every edge at least once to determine bipartiteness. The BFS approach is more straightforward and is typically the one you should reach for first. The Union-Find approach is worth knowing as an alternative, especially when the problem involves dynamic edge additions or when you are already using Union-Find for connected component tracking.
You're probably looking at a bipartite check when:
Common templates: