785. Is Graph Bipartite
Problem Description
You are given an undirected graph with n
nodes numbered from 0
to n - 1
. The graph is represented as a 2D array graph
, where graph[u]
contains all nodes that are adjacent to node u
.
The graph has these properties:
- No self-edges (a node doesn't connect to itself)
- No parallel edges (no duplicate connections between the same pair of nodes)
- Undirected edges (if node
v
is ingraph[u]
, then nodeu
is ingraph[v]
) - May not be connected (some nodes might not have a path between them)
A graph is bipartite if you can split all nodes into two independent sets A
and B
such that every edge connects a node from set A
to a node from set B
. In other words, no two nodes within the same set are connected.
Your task is to determine if the given graph is bipartite. Return true
if it is bipartite, otherwise return false
.
The solution uses a coloring approach with DFS. The idea is to try coloring the graph using two colors (represented as 1
and -1
). Starting from each uncolored node, we assign it one color and recursively assign the opposite color to all its neighbors. If at any point we find a neighbor that has already been colored with the same color as the current node, the graph cannot be bipartite. The color
array tracks the color of each node, where 0
means uncolored, 1
represents one color, and -1
represents the other color.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem explicitly states we have an undirected graph with
n
nodes, wheregraph[u]
contains all adjacent nodes to nodeu
.
Is it a tree?
- No: The graph may have cycles (not mentioned to be acyclic) and may not even be connected. Trees must be connected and acyclic.
Is the problem related to directed acyclic graphs?
- No: The graph is undirected, not directed.
Is the problem related to shortest paths?
- No: We're not finding shortest paths between nodes. We're determining if the graph can be colored with two colors (bipartite property).
Does the problem involve connectivity?
- No: While the graph may not be connected, we're not specifically solving connectivity problems like finding connected components or checking if nodes are connected.
Does the problem have small constraints?
- Yes: Although not explicitly stated in this problem description, graph coloring problems often have manageable constraints that allow for DFS exploration.
DFS/backtracking?
- Yes: DFS is perfect for this problem as we need to traverse the graph and color nodes, checking if we can maintain the bipartite property (alternating colors).
Conclusion: The flowchart suggests using a DFS approach. This aligns perfectly with the solution, which uses DFS to traverse the graph and attempt to color it with two colors. Starting from each uncolored node, we assign it a color and recursively color all its neighbors with the opposite color. If we encounter a conflict (a neighbor already has the same color), the graph is not bipartite.
Intuition
The key insight for determining if a graph is bipartite comes from understanding what a bipartite graph fundamentally represents: a graph where nodes can be divided into two groups such that all edges connect nodes from different groups.
Think of it like organizing a party where some guests don't get along. You want to split them into two rooms such that people who don't get along (connected by an edge) are in different rooms. If you can successfully do this, the graph is bipartite.
This naturally leads to a coloring problem. Imagine painting nodes with two colors - let's say red and blue. If we can color the entire graph such that no two adjacent nodes have the same color, then we've successfully partitioned the nodes into two independent sets (red nodes and blue nodes).
The process works like this:
- Start with any uncolored node and paint it red (color =
1
) - All its neighbors must be blue (color =
-1
) - All neighbors of those blue nodes must be red
- Continue this alternating pattern
If at any point we try to paint a node and find that it's already painted with the wrong color, we've discovered a conflict - the graph cannot be bipartite. This happens when we encounter an odd-length cycle in the graph.
Why DFS? Because we need to propagate the coloring constraint through connected components. When we color a node, we immediately need to color all its neighbors with the opposite color, and then their neighbors, and so on. DFS naturally follows this propagation pattern.
The reason we loop through all nodes in the main function is that the graph might not be connected. We need to check each disconnected component separately - if any component is not bipartite, the entire graph is not bipartite.
Learn more about Depth-First Search, Breadth-First Search, Union Find and Graph patterns.
Solution Approach
The solution implements the coloring method to determine if a graph is bipartite using Depth-First Search (DFS).
Data Structure:
color
array: Tracks the color of each node where:0
means uncolored (not visited yet)1
represents one color (e.g., red)-1
represents the opposite color (e.g., blue)
Implementation Details:
-
DFS Helper Function
dfs(a, c)
:- Takes node
a
and colorc
as parameters - Colors node
a
with colorc
:color[a] = c
- Iterates through all neighbors
b
of nodea
- For each neighbor, checks two conditions:
- If
color[b] == c
: The neighbor already has the same color as the current node - conflict detected, returnFalse
- If
color[b] == 0
: The neighbor is uncolored, so recursively color it with the opposite colordfs(b, -c)
. If this recursive call returnsFalse
, propagate the failure
- If
- If all neighbors are successfully processed, return
True
- Takes node
-
Main Logic:
- Initialize the
color
array with zeros for alln
nodes - Iterate through each node
i
from0
ton-1
- For each uncolored node (
color[i] == 0
):- Start DFS coloring from this node with color
1
- If DFS returns
False
, the graph is not bipartite, returnFalse
- Start DFS coloring from this node with color
- If all components are successfully colored, return
True
- Initialize the
Why This Works:
The algorithm exploits the property that in a bipartite graph, nodes can be colored with two colors such that no adjacent nodes share the same color. The DFS ensures that once we color a node, all nodes in its connected component get colored consistently.
The clever use of -c
to alternate colors ensures that adjacent nodes always get opposite colors. Starting with color 1
, neighbors get -1
, their neighbors get -(-1) = 1
, and so on, creating the alternating pattern needed for bipartite graphs.
The outer loop handles disconnected components - each component is checked independently, and the graph is bipartite only if all components are bipartite.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let me walk through a small example to illustrate the solution approach.
Example Graph:
graph = [[1,3], [0,2], [1,3], [0,2]]
This represents:
- Node 0 connects to nodes 1 and 3
- Node 1 connects to nodes 0 and 2
- Node 2 connects to nodes 1 and 3
- Node 3 connects to nodes 0 and 2
Visual representation:
0 --- 1 | | | | 3 --- 2
Step-by-step execution:
-
Initialize:
color = [0, 0, 0, 0]
(all nodes uncolored) -
Process node 0:
color[0] == 0
, so start DFS from node 0 with color1
- DFS(0, 1):
- Color node 0:
color = [1, 0, 0, 0]
- Check neighbor 1: uncolored, so call DFS(1, -1)
- Color node 0:
-
DFS(1, -1):
- Color node 1:
color = [1, -1, 0, 0]
- Check neighbor 0: already colored with
1
(different from-1
), OK - Check neighbor 2: uncolored, so call DFS(2, 1)
- Color node 1:
-
DFS(2, 1):
- Color node 2:
color = [1, -1, 1, 0]
- Check neighbor 1: already colored with
-1
(different from1
), OK - Check neighbor 3: uncolored, so call DFS(3, -1)
- Color node 2:
-
DFS(3, -1):
- Color node 3:
color = [1, -1, 1, -1]
- Check neighbor 0: already colored with
1
(different from-1
), OK - Check neighbor 2: already colored with
1
(different from-1
), OK - Return
True
- Color node 3:
-
Back to main loop:
- Node 1: already colored, skip
- Node 2: already colored, skip
- Node 3: already colored, skip
- All nodes processed successfully, return
True
Final coloring: color = [1, -1, 1, -1]
This means:
- Set A (color 1): nodes {0, 2}
- Set B (color -1): nodes {1, 3}
All edges connect nodes from different sets, confirming the graph is bipartite.
Counter-example (non-bipartite):
If we had graph = [[1,2], [0,2], [0,1]]
(a triangle), the algorithm would:
- Color node 0 with
1
- Color node 1 with
-1
(neighbor of 0) - Color node 2 with
-1
(neighbor of 0) - When processing node 1's neighbors, find node 2 already colored with
-1
(same color) - Return
False
- conflict detected!
Solution Implementation
1class Solution:
2 def isBipartite(self, graph: List[List[int]]) -> bool:
3 """
4 Determines if an undirected graph is bipartite.
5 A graph is bipartite if nodes can be divided into two disjoint sets
6 such that no two adjacent nodes are in the same set.
7
8 Args:
9 graph: Adjacency list representation where graph[i] contains neighbors of node i
10
11 Returns:
12 True if the graph is bipartite, False otherwise
13 """
14
15 def dfs(node: int, node_color: int) -> bool:
16 """
17 Performs depth-first search to color the graph with two colors.
18
19 Args:
20 node: Current node to color
21 node_color: Color to assign to current node (1 or -1)
22
23 Returns:
24 True if coloring is valid, False if conflict found
25 """
26 # Assign color to current node
27 colors[node] = node_color
28
29 # Check all neighbors of current node
30 for neighbor in graph[node]:
31 # If neighbor has same color as current node, graph is not bipartite
32 if colors[neighbor] == node_color:
33 return False
34
35 # If neighbor is uncolored, recursively color it with opposite color
36 # If coloring fails, return False
37 if colors[neighbor] == 0 and not dfs(neighbor, -node_color):
38 return False
39
40 return True
41
42 # Initialize variables
43 num_nodes = len(graph)
44 colors = [0] * num_nodes # 0: uncolored, 1: color A, -1: color B
45
46 # Process each connected component
47 for node_idx in range(num_nodes):
48 # If node is uncolored, start DFS from this node
49 # If any component cannot be bipartitioned, return False
50 if colors[node_idx] == 0 and not dfs(node_idx, 1):
51 return False
52
53 # All components successfully colored with two colors
54 return True
55
1class Solution {
2 // Array to store the color of each node (1 or -1 for two different colors, 0 for unvisited)
3 private int[] nodeColors;
4 // Reference to the input graph
5 private int[][] adjacencyList;
6
7 /**
8 * Determines if the given graph is bipartite.
9 * A graph is bipartite if nodes can be divided into two disjoint sets
10 * such that no two adjacent nodes share the same set.
11 *
12 * @param graph The adjacency list representation of the graph
13 * @return true if the graph is bipartite, false otherwise
14 */
15 public boolean isBipartite(int[][] graph) {
16 int numberOfNodes = graph.length;
17 nodeColors = new int[numberOfNodes];
18 adjacencyList = graph;
19
20 // Process each connected component of the graph
21 for (int node = 0; node < numberOfNodes; ++node) {
22 // If node is unvisited, start DFS coloring from this node
23 if (nodeColors[node] == 0 && !dfsColoring(node, 1)) {
24 return false;
25 }
26 }
27 return true;
28 }
29
30 /**
31 * Performs DFS to color the graph nodes with alternating colors.
32 *
33 * @param currentNode The current node being processed
34 * @param currentColor The color to assign to the current node (1 or -1)
35 * @return true if coloring is successful (bipartite), false otherwise
36 */
37 private boolean dfsColoring(int currentNode, int currentColor) {
38 // Assign color to current node
39 nodeColors[currentNode] = currentColor;
40
41 // Check all neighbors of the current node
42 for (int neighbor : adjacencyList[currentNode]) {
43 // If neighbor has same color as current node, graph is not bipartite
44 if (nodeColors[neighbor] == currentColor) {
45 return false;
46 }
47 // If neighbor is unvisited, recursively color it with opposite color
48 if (nodeColors[neighbor] == 0 && !dfsColoring(neighbor, -currentColor)) {
49 return false;
50 }
51 }
52 return true;
53 }
54}
55
1class Solution {
2public:
3 bool isBipartite(vector<vector<int>>& graph) {
4 int numNodes = graph.size();
5 // Color array: 0 = unvisited, 1 = color A, -1 = color B
6 vector<int> nodeColors(numNodes, 0);
7
8 // DFS function to color the graph
9 // Parameters: current node and color to assign
10 // Returns: true if coloring is valid, false if conflict found
11 auto dfsColoring = [&](this auto&& dfsColoring, int currentNode, int assignedColor) -> bool {
12 // Assign color to current node
13 nodeColors[currentNode] = assignedColor;
14
15 // Check all neighbors of current node
16 for (int neighbor : graph[currentNode]) {
17 // If neighbor has same color as current node, graph is not bipartite
18 if (nodeColors[neighbor] == assignedColor) {
19 return false;
20 }
21
22 // If neighbor is unvisited, recursively color it with opposite color
23 // If coloring fails, propagate the failure
24 if (nodeColors[neighbor] == 0 && !dfsColoring(neighbor, -assignedColor)) {
25 return false;
26 }
27 }
28
29 return true;
30 };
31
32 // Process all connected components
33 for (int node = 0; node < numNodes; ++node) {
34 // Start DFS from unvisited nodes (handles disconnected components)
35 if (nodeColors[node] == 0 && !dfsColoring(node, 1)) {
36 return false;
37 }
38 }
39
40 // All nodes successfully colored with two colors
41 return true;
42 }
43};
44
1/**
2 * Determines if an undirected graph is bipartite.
3 * A graph is bipartite if we can split its set of nodes into two independent subsets
4 * such that every edge connects a node in one subset to a node in the other subset.
5 * @param graph - Adjacency list representation of the graph
6 * @returns true if the graph is bipartite, false otherwise
7 */
8function isBipartite(graph: number[][]): boolean {
9 const nodeCount: number = graph.length;
10 // Color array to track node colors: 0 (unvisited), 1 (color A), -1 (color B)
11 const nodeColors: number[] = Array(nodeCount).fill(0);
12
13 /**
14 * Performs depth-first search to color nodes alternately.
15 * @param currentNode - The node to color
16 * @param assignedColor - The color to assign to the current node (1 or -1)
17 * @returns true if coloring is successful without conflicts, false otherwise
18 */
19 const depthFirstSearch = (currentNode: number, assignedColor: number): boolean => {
20 // Assign color to the current node
21 nodeColors[currentNode] = assignedColor;
22
23 // Check all adjacent nodes
24 for (const adjacentNode of graph[currentNode]) {
25 // If adjacent node has the same color, graph is not bipartite
26 if (nodeColors[adjacentNode] === assignedColor) {
27 return false;
28 }
29
30 // If adjacent node is unvisited, recursively color it with opposite color
31 if (nodeColors[adjacentNode] === 0 && !depthFirstSearch(adjacentNode, -assignedColor)) {
32 return false;
33 }
34 }
35
36 return true;
37 };
38
39 // Process all connected components in the graph
40 for (let nodeIndex = 0; nodeIndex < nodeCount; nodeIndex++) {
41 // Start DFS from unvisited nodes with color 1
42 if (nodeColors[nodeIndex] === 0 && !depthFirstSearch(nodeIndex, 1)) {
43 return false;
44 }
45 }
46
47 return true;
48}
49
Time and Space Complexity
The time complexity is O(V + E)
, where V
is the number of vertices (nodes) and E
is the number of edges in the graph. The DFS traversal visits each vertex once, taking O(V)
time. For each vertex, we iterate through all its adjacent vertices (edges), which in total across all vertices takes O(E)
time. Therefore, the overall time complexity is O(V + E)
.
Note that the reference answer states O(n)
where n
is the number of nodes. This is a simplified notation that assumes the graph is sparse (where E = O(V)
), making the complexity O(V)
or equivalently O(n)
.
The space complexity is O(V)
or O(n)
, where n
is the number of nodes. This accounts for:
- The
color
array which stores the color for each vertex:O(n)
- The recursive call stack in the worst case (for a path-like graph):
O(n)
Therefore, the total space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to Handle Disconnected Components
The Pitfall: A common mistake is assuming the graph is connected and only checking from node 0. This would miss disconnected components that might not be bipartite.
# WRONG: Only checks the component containing node 0
def isBipartite(self, graph: List[List[int]]) -> bool:
colors = [0] * len(graph)
return dfs(0, 1) # Misses other components!
Why It's Wrong: If the graph has multiple disconnected components, nodes in other components would remain uncolored (color = 0) and never get checked. A non-bipartite component could be missed entirely.
The Solution: Always iterate through ALL nodes and start DFS from any uncolored node:
for node_idx in range(num_nodes):
if colors[node_idx] == 0 and not dfs(node_idx, 1):
return False
2. Incorrect Neighbor Color Check Logic
The Pitfall: Checking if a neighbor is already colored with the opposite color and trying to recolor it:
# WRONG: Attempting to recolor already colored nodes
def dfs(node, node_color):
colors[node] = node_color
for neighbor in graph[node]:
if colors[neighbor] != 0 and colors[neighbor] != -node_color:
return False
if not dfs(neighbor, -node_color): # This would recolor already colored nodes!
return False
return True
Why It's Wrong: This would attempt to revisit and recolor nodes that are already colored, leading to infinite recursion or incorrect results. Once a node is colored, it should not be recolored.
The Solution: Only recursively color uncolored neighbors:
for neighbor in graph[node]: if colors[neighbor] == node_color: # Same color = conflict return False if colors[neighbor] == 0 and not dfs(neighbor, -node_color): # Only color if uncolored return False
3. Using BFS Without Proper Queue Initialization
The Pitfall: When converting to BFS, forgetting to color the starting node before adding to queue:
# WRONG: Not coloring the start node before BFS
def isBipartite(self, graph: List[List[int]]) -> bool:
colors = [0] * len(graph)
for i in range(len(graph)):
if colors[i] == 0:
queue = deque([i]) # Added to queue but not colored!
while queue:
node = queue.popleft()
for neighbor in graph[node]:
# Logic fails because node's color isn't set
The Solution: Always color the starting node before beginning BFS:
if colors[i] == 0: colors[i] = 1 # Color the starting node first queue = deque([i])
Which of the following is a min heap?
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Union Find Disjoint Set Union Data Structure Introduction Prerequisite Depth First Search Review problems dfs_intro Once we have a strong grasp of recursion and Depth First Search we can now introduce Disjoint Set Union DSU This data structure is motivated by the following problem Suppose we have sets of elements
Want a Structured Path to Master System Design Too? Donβt Miss This!