Facebook Pixel

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 in graph[u], then node u is in graph[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, where graph[u] contains all adjacent nodes to node u.

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Start with any uncolored node and paint it red (color = 1)
  2. All its neighbors must be blue (color = -1)
  3. All neighbors of those blue nodes must be red
  4. 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:

  1. DFS Helper Function dfs(a, c):

    • Takes node a and color c as parameters
    • Colors node a with color c: color[a] = c
    • Iterates through all neighbors b of node a
    • For each neighbor, checks two conditions:
      • If color[b] == c: The neighbor already has the same color as the current node - conflict detected, return False
      • If color[b] == 0: The neighbor is uncolored, so recursively color it with the opposite color dfs(b, -c). If this recursive call returns False, propagate the failure
    • If all neighbors are successfully processed, return True
  2. Main Logic:

    • Initialize the color array with zeros for all n nodes
    • Iterate through each node i from 0 to n-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, return False
    • If all components are successfully colored, return True

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 Evaluator

Example 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:

  1. Initialize: color = [0, 0, 0, 0] (all nodes uncolored)

  2. Process node 0:

    • color[0] == 0, so start DFS from node 0 with color 1
    • DFS(0, 1):
      • Color node 0: color = [1, 0, 0, 0]
      • Check neighbor 1: uncolored, so call DFS(1, -1)
  3. 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)
  4. DFS(2, 1):

    • Color node 2: color = [1, -1, 1, 0]
    • Check neighbor 1: already colored with -1 (different from 1), OK
    • Check neighbor 3: uncolored, so call DFS(3, -1)
  5. 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
  6. 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:

  1. Color node 0 with 1
  2. Color node 1 with -1 (neighbor of 0)
  3. Color node 2 with -1 (neighbor of 0)
  4. When processing node 1's neighbors, find node 2 already colored with -1 (same color)
  5. 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])
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following is a min heap?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More