785. Is Graph Bipartite? - Explanation
Description
The undirected graph has nodes numbered from 0 through n-1. Each list graph[u] contains the neighbors of node u. Decide whether you can assign every node to one of two groups so that each edge has one endpoint in each group. Return true if such an assignment exists, and false otherwise. The graph may have several disconnected components or isolated nodes; all components must satisfy the rule. There are no self-loops or repeated neighbors, and each edge appears in both endpoints' lists.
Example 1:
Input: graph = [[1],[0,2],[1]]
Output: trueExplanation: Place nodes 0 and 2 in one group and node 1 in the other. Both edges cross between the groups, so the graph is bipartite.
Example 2:
Input: graph = [[1,2],[0,2],[0,1]]
Output: falseExplanation: These three nodes form a triangle. Nodes 1 and 2 must both be opposite node 0, which puts them in the same group despite the edge between them. No valid assignment exists.
Example 3:
Input: graph = [[],[]]
Output: trueExplanation: Neither node has any neighbors. There are no edges to violate the grouping rule, so the graph is bipartite.
Constraints:
1 <= n <= 100- Each neighbor list contains fewer than
nnode numbers, all between0andn-1. - A node is never its own neighbor, and each neighbor appears at most once.
- If
vis listed ingraph[u], thenuis listed ingraph[v]. - Neighbor lists need not be sorted.
Topics
Prerequisites
Before attempting this problem, you should be comfortable with:
- Graph Representation - Understanding adjacency lists and how graphs are stored in memory
- Depth-First Search (DFS) - Traversing graphs recursively or with an explicit stack
- Breadth-First Search (BFS) - Level-order traversal of graphs using a queue
- Union-Find (Disjoint Set Union) - Efficiently grouping and querying connected components
1. Depth First Search
Intuition
A graph is bipartite if we can split its nodes into two groups such that every edge connects nodes from different groups. This is equivalent to checking if the graph is 2-colorable. Using DFS, we assign a color to the starting node and then assign the opposite color to all its neighbors. If we ever find a neighbor that already has the same color as the current node, the graph is not bipartite. Since the graph may be disconnected, we run dfs from every unvisited node.
Algorithm
- Create a
colorarray initialized to0(unvisited). Use1and-1as the two colors. - Define a recursive
dfsfunction that takes a nodeiand a colorc:- Assign
color[i] = c. - For each neighbor, if it has the same color, return
false. - If the neighbor is unvisited, recursively call
dfswith the opposite color. If that returnsfalse, propagate the failure. - Return
trueif all neighbors pass.
- Assign
- For each node, if unvisited, run
dfswith color1. If anydfsfails, returnfalse. - Return
trueif all components are bipartite.
class Solution:
def isBipartite(self, graph: List[List[int]]) -> bool:
color = [0] * len(graph) # Map node i -> odd=1, even=-1
def dfs(i, c):
color[i] = c
for nei in graph[i]:
if color[nei] == c:
return False
if color[nei] == 0 and not dfs(nei, -c):
return False
return True
for i in range(len(graph)):
if color[i] == 0 and not dfs(i, 1):
return False
return TrueTime & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of vertices and is the number of edges.
2. Breadth First Search
Intuition
BFS provides another way to check 2-colorability. Starting from an uncolored node, we assign it a color and add it to a queue. For each node we process, we check all neighbors: if a neighbor has the same color, the graph is not bipartite; if uncolored, we assign it the opposite color and enqueue it. BFS naturally explores the graph level by level, alternating colors between levels.
Algorithm
- Create a
colorarray initialized to0. - For each node
ifrom0ton-1:- If already colored, skip it.
- Initialize a queue with node
iand setcolor[i] = -1. - While the queue is not empty:
- Dequeue a node.
- For each neighbor: if it has the same color, return
false. If uncolored, assign the opposite color and enqueue it.
- Return
trueif all nodes are processed without conflict.
class Solution:
def isBipartite(self, graph: List[List[int]]) -> bool:
color = [0] * len(graph) # Map node i -> odd=1, even=-1
def bfs(i):
if color[i]:
return True
q = deque([i])
color[i] = -1
while q:
i = q.popleft()
for nei in graph[i]:
if color[i] == color[nei]:
return False
elif not color[nei]:
q.append(nei)
color[nei] = -1 * color[i]
return True
for i in range(len(graph)):
if not bfs(i):
return False
return TrueTime & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of vertices and is the number of edges.
3. Iterative DFS
Intuition
Iterative DFS uses an explicit stack instead of recursion to traverse the graph. The coloring logic remains the same: assign a color to a node, then process all neighbors by checking for conflicts and pushing uncolored neighbors onto the stack with the opposite color. This avoids potential stack overflow issues with very deep graphs while achieving the same result as recursive DFS.
Algorithm
- Create a
colorarray initialized to0. - For each node
ifrom0ton-1:- If already colored, skip it.
- Set
color[i] = -1and pushionto the stack. - While the stack is not empty:
- Pop a node.
- For each neighbor: if it has the same color, return
false. If uncolored, assign the opposite color and push it onto the stack.
- Return
trueif no conflicts are found.
class Solution:
def isBipartite(self, graph: List[List[int]]) -> bool:
color = [0] * len(graph) # Map node i -> odd=1, even=-1
stack = []
for i in range(len(graph)):
if color[i] != 0:
continue
color[i] = -1
stack.append(i)
while stack:
node = stack.pop()
for nei in graph[node]:
if color[node] == color[nei]:
return False
elif not color[nei]:
stack.append(nei)
color[nei] = -1 * color[node]
return TrueTime & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of vertices and is the number of edges.
4. Disjoint Set Union
Intuition
DSU (Union-Find) offers an alternative perspective. For a bipartite graph, a node must be in a different set than all of its neighbors, but all neighbors should belong to the same set. For each node, we union all its neighbors together. If a node ever ends up in the same set as one of its neighbors, the graph is not bipartite. This works because being in the same connected component in the neighbor graph implies they must share a color in a valid 2-coloring.
Algorithm
- Initialize a DSU structure with
nnodes. - For each node:
- If it has no neighbors, continue.
- For each neighbor:
- If the node and neighbor are in the same set, return
false. - Union the neighbor with the first neighbor of the current node (grouping all neighbors together).
- If the node and neighbor are in the same set, return
- Return
trueif no conflict is detected.
class DSU:
def __init__(self, n):
self.Parent = list(range(n))
self.Size = [0] * n
def find(self, node):
if self.Parent[node] != node:
self.Parent[node] = self.find(self.Parent[node])
return self.Parent[node]
def union(self, u, v):
pu, pv = self.find(u), self.find(v)
if pu == pv:
return False
if self.Size[pu] > self.Size[pv]:
self.Parent[pv] = pu
elif self.Size[pu] < self.Size[pv]:
self.Parent[pu] = pv
else:
self.Parent[pv] = pu
self.Size[pu] += 1
return True
class Solution:
def isBipartite(self, graph: List[List[int]]) -> bool:
n = len(graph)
dsu = DSU(n)
for node in range(n):
for nei in graph[node]:
if dsu.find(node) == dsu.find(nei):
return False
dsu.union(graph[node][0], nei)
return TrueTime & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of vertices and is the number of edges. is used for amortized complexity.
Common Pitfalls
Forgetting to Handle Disconnected Components
The graph may consist of multiple disconnected components. If you only run BFS/DFS from node 0, you will miss other components that might not be bipartite. Always iterate through all nodes and start a new traversal from any unvisited node to ensure every component is checked.
Checking Only Unvisited Neighbors
When traversing the graph, you must check the color of all neighbors, not just unvisited ones. If a neighbor is already colored with the same color as the current node, the graph is not bipartite. Skipping already-visited neighbors misses these conflict cases.
Using Wrong Initial Color Values
Using 0 as a valid color while also using it to represent "unvisited" creates ambiguity. A clean approach is to use 0 for unvisited nodes and 1/-1 as the two actual colors. Alternatively, use a separate visited array. Mixing up these states leads to incorrect bipartiteness detection.
Sign in to join the discussion