Is Graph Bipartite?
MedPrompt
There is an undirected graph with n nodes, where each node is numbered between 0 and n - 1. You are given a 2D array graph, where graph[u] is an array of nodes that node u is adjacent to. More formally, for each v in graph[u], there is an undirected edge between node u and node v. The graph has the following properties:
- There are no self-edges (
graph[u]does not containu). - There are no parallel edges (
graph[u]does not contain duplicate values). - If
vis ingraph[u], thenuis ingraph[v](the graph is undirected). - The graph may not be connected, meaning there may be two nodes
uandvsuch that there is no path between them.
A graph is bipartite if the nodes can be partitioned into two independent sets A and B such that every edge in the graph connects a node in set A and a node in set B.
Return true if and only if it is bipartite.
Example 1:
Input: graph = [[1,2,3],[0,2],[0,1,3],[0,2]]
Output: false
Explanation: There is no way to partition the nodes into two independent sets such that every edge connects a node in one and a node in the other.Example 2:
Input: graph = [[1,3],[0,2],[1,3],[0,2]]
Output: true
Explanation: We can partition the nodes into two sets: {0, 2} and {1, 3}.
Constraints:
graph.length == n1 <= n <= 1000 <= graph[u].length < n0 <= graph[u][i] <= n - 1graph[u]does not containu.- All the values of
graph[u]are unique. - If
graph[u]containsv, thengraph[v]containsu.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach uses a recursive Depth-First Search (DFS) traversal to check for bipartiteness. The core idea is to assign one of two colors (e.g., 1 and -1) to each node. We start from an arbitrary uncolored node, assign it the first color, and then traverse its neighbors. Each neighbor is assigned the opposite color. If we encounter a neighbor that is already colored with the same color as the current node, we have found an odd-length cycle, which means the graph is not bipartite. Since the graph might be disconnected, we iterate through all nodes to ensure every component is checked.
Algorithm
- Initialize a
colorsarray of sizenwith0s (representing uncolored nodes). - Iterate through each node
ifrom0ton-1. - If node
iis uncolored (colors[i] == 0), it means we have found a new, unvisited component of the graph. - Start a recursive Depth-First Search (DFS) from this node. Call a helper function, e.g.,
dfs(graph, colors, i, 1), assigning it an initial color (e.g., 1). - If the
dfscall ever returnsfalse, it means a conflict was found in that component, so the graph is not bipartite. Returnfalseimmediately. - The
dfs(node, color)helper function:- Assign the given
colorto the currentnodeby settingcolors[node] = color. - For each
neighborof the currentnode:- If the
neighboris uncolored (colors[neighbor] == 0), recursively calldfson it with the opposite color:dfs(graph, colors, neighbor, -color). If this recursive call returnsfalse, propagate thefalseresult up. - If the
neighboris already colored, check if its color is the same as the current node's color (colors[neighbor] == color). If it is, a conflict is found, so returnfalse.
- If the
- Assign the given
- If the main loop finishes without any
dfscall returningfalse, it means all components were successfully two-colored. Returntrue.
Walkthrough
We use an auxiliary array, colors, to store the color of each node. A value of 0 means uncolored, 1 means color A, and -1 means color B.
The main function iterates through all nodes. If a node hasn't been colored yet, we start a DFS from it with an initial color (e.g., 1).
The recursive DFS function dfs(node, color) works as follows:
- Color the current
nodewith the givencolor. - For each
neighborof thenode:- If the
neighboris uncolored, recursively calldfs(neighbor, -color). If this recursive call returnsfalse, it means a conflict was found deeper in the traversal, so we propagatefalseup. - If the
neighboris already colored, check ifcolors[neighbor] == colors[node]. If they have the same color, it violates the bipartite property, so we returnfalse.
- If the
- If the loop finishes without finding any conflicts, it means this part of the graph is bipartite, so we return
true. If the main loop completes and all components are successfully colored, the entire graph is bipartite.
class Solution { public boolean isBipartite(int[][] graph) { int n = graph.length; int[] colors = new int[n]; // 0: uncolored, 1: color A, -1: color B for (int i = 0; i < n; i++) { // If the node is not yet colored, start a DFS from it. if (colors[i] == 0) { if (!dfs(graph, colors, i, 1)) { return false; } } } return true; } private boolean dfs(int[][] graph, int[] colors, int node, int color) { colors[node] = color; for (int neighbor : graph[node]) { if (colors[neighbor] == 0) { // If neighbor is uncolored, color it with the opposite color and recurse. if (!dfs(graph, colors, neighbor, -color)) { return false; } } else if (colors[neighbor] == color) { // If neighbor has the same color, conflict found. return false; } } return true; }}Complexity
Time
O(V + E), where V is the number of vertices and E is the number of edges. Each vertex is visited once, and each edge is checked once across all DFS calls.
Space
O(V), where V is the number of vertices. This space is used for the `colors` array and the recursion call stack. In the worst-case scenario of a path graph, the recursion depth can be up to V.
Trade-offs
Pros
Conceptually straightforward and easy to implement using recursion.
Optimal time complexity for solving this problem.
Cons
The recursive implementation can lead to a
StackOverflowErrorif the graph is very deep (i.e., has a very long path). However, given the constraintn <= 100, this is not a practical concern for this specific problem.
This approach uses an iterative Breadth-First Search (BFS) traversal. Similar to the DFS method, it attempts to color the graph's nodes with two alternating colors. We use a queue to manage the nodes to visit. When we process a node, we color its uncolored neighbors with the opposite color and add them to the queue. If we find a neighbor that is already colored with the same color as the current node, we've detected a conflict (an odd-length cycle), and the graph is not bipartite. This process is repeated for all disconnected components of the graph.
Algorithm
- Initialize a
colorsarray of sizenwith0s (uncolored). - Iterate through each node
ifrom0ton-1. - If node
iis uncolored (colors[i] == 0), start a BFS traversal for this component.- Create a
Queueand addito it. - Color the starting node
iwith an initial color, e.g.,colors[i] = 1. - While the queue is not empty:
- Dequeue a node, let's call it
u. - For each
neighborvofu:- If
vis uncolored (colors[v] == 0), color it with the opposite color ofu(colors[v] = -colors[u]) and enqueuev. - If
vis already colored, check if its color is the same asu's (colors[v] == colors[u]). If so, a conflict exists, and the graph is not bipartite. Returnfalse.
- If
- Dequeue a node, let's call it
- Create a
- If the main loop completes without finding any conflicts, it means all components are bipartite. Return
true.
Walkthrough
We maintain a colors array, where 0 indicates an uncolored node, 1 represents color A, and -1 represents color B. We also use a Queue for the BFS traversal.
The algorithm iterates through all nodes to handle potentially disconnected graphs. If a node is uncolored, we start a BFS traversal from it:
- Assign an initial color (e.g., 1) to the starting node and add it to the queue.
- While the queue is not empty, dequeue a node
u. - For each
neighborvofu:- If
vis uncolored (colors[v] == 0), assign it the opposite color ofu(-colors[u]) and enqueue it. - If
vis already colored andcolors[v] == colors[u], it means two adjacent nodes have the same color. This is a conflict, so we returnfalse.
- If
- If the BFS for a component completes without any conflicts, we continue the main loop.
If the entire loop finishes without returning
false, the graph is bipartite.
import java.util.Queue;import java.util.LinkedList; class Solution { public boolean isBipartite(int[][] graph) { int n = graph.length; int[] colors = new int[n]; // 0: uncolored, 1: color A, -1: color B for (int i = 0; i < n; i++) { if (colors[i] == 0) { // This node belongs to a component we haven't visited yet Queue<Integer> queue = new LinkedList<>(); queue.add(i); colors[i] = 1; // Start coloring with color 1 while (!queue.isEmpty()) { int node = queue.poll(); for (int neighbor : graph[node]) { if (colors[neighbor] == 0) { // If neighbor is uncolored, color with opposite color and add to queue colors[neighbor] = -colors[node]; queue.add(neighbor); } else if (colors[neighbor] == colors[node]) { // If neighbor has the same color, conflict found return false; } } } } } return true; }}Complexity
Time
O(V + E), where V is the number of vertices and E is the number of edges. Each vertex is enqueued and dequeued at most once, and every edge is examined once.
Space
O(V), where V is the number of vertices. This space is required for the `colors` array and the `Queue`. In the worst case, the queue can hold up to O(V) nodes (e.g., in a star graph).
Trade-offs
Pros
Avoids recursion, thus preventing any potential for stack overflow errors, making it more robust for very large or deep graphs.
Optimal time and space complexity.
Often slightly faster in practice than recursive DFS due to lower overhead from function calls.
Cons
The iterative logic using a queue might be slightly more complex to write for some developers compared to the recursive DFS approach.
Solutions
Solution
class Solution {private int[] color;private int[][] g;public boolean isBipartite(int[][] graph) { int n = graph.length; color = new int[n]; g = graph; for (int i = 0; i < n; ++i) { if (color[i] == 0 && !dfs(i, 1)) { return false; } } return true; }private boolean dfs(int u, int c) { color[u] = c; for (int v : g[u]) { if (color[v] == 0) { if (!dfs(v, 3 - c)) { return false; } } else if (color[v] == c) { return false; } } return true; }}class Solution {public: bool isBipartite(vector<vector<int>> &graph) { int n = graph.size(); vector<int> color(n); for (int i = 0; i < n; ++i) if (!color[i] && !dfs(i, 1, color, graph)) return false; return true; } bool dfs(int u, int c, vector<int> &color, vector<vector<int>> &g) { color[u] = c; for (int &v : g[u]) { if (!color[v]) { if (!dfs(v, 3 - c, color, g)) return false; } else if (color[v] == c) return false; } return true; }};class Solution: def isBipartite(self, graph: List[List[int]]) -> bool: def dfs(u, c): color[u] = c for v in graph[u]: if not color[v]: if not dfs(v, 3 - c): return False elif color[v] == c: return False return True n = len(graph) color = [0] * n for i in range(n): if not color[i] and not dfs(i, 1): return False return TrueVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.