Grokking the Coding Interview: Patterns for Coding Questions
Ask Author
Back to course home

0% completed

Vote For New Content
Solution: Is Graph Bipartite?
Table of Contents

Contents are not accessible

Contents are not accessible

Contents are not accessible

Contents are not accessible

Contents are not accessible

Problem Statement

Given a 2D array graph[][], representing the undirected graph, where graph[u] is an array of nodes that are connected with node u.

Determine whether a given undirected graph is a bipartite graph.

The graph is a bipartite graph, if we can split the set of nodes into two distinct subsets such that no two nodes within the same subset are adjacent (i.e., no edge exists between any two nodes within a single subset).

Examples

  1. Example 1:
    • Input: graph = [[1,3], [0,2], [1,3], [0,2]]
Image
  • Expected Output: true
  • Justification: The nodes can be divided into two groups: {0, 2} and {1, 3}. No two nodes within each group are adjacent, thus the graph is bipartite.
  1. Example 2:
    • Input: graph = [[1], [0], [3], [2]]
Image
  • Expected Output: true
  • Justification: The graph is a simple chain of 4 nodes. It's clearly bipartite as we can have {0, 2} and {1, 3} as the two subsets.
  1. Example 3:
    • Input: graph = [[1,2,3],[0,2],[0,1,3],[0,2]]
Image
  • Expected Output: false
  • Justification: We found that edges (1, 2), (0, 3), and (2, 3) connect nodes within the same set, which violates the condition for a bipartite graph where each edge must connect nodes in different subsets. Thus, there's no way to divide this graph into two subsets that satisfy the bipartite condition.

Constraints:

  • graph.length == n
  • 1 <= n <= 100
  • 0 <= graph[u].length < n
  • 0 <= graph[u][i] <= n - 1
  • graph[u] does not contain u.
  • All the values of graph[u] are unique.
  • If graph[u] contains v, then graph[v] contains u.

Solution

Algorithm Description

The algorithm for determining if a graph is bipartite uses the Union-Find data structure, enhanced with path compression and union by rank. The primary goal is to ensure that no two adjacent nodes are in the same set, which would indicate that the graph is not bipartite.

Initially, each node is its own parent, and all ranks are set to zero. The algorithm then iterates through each node and its neighbors. For each node, it selects the first neighbor as a representative to which other neighbors will be compared. This is done to ensure that all neighbors of a node are placed in the opposite set. By attempting to union each neighbor with the first neighbor, we effectively check if all neighbors can be grouped together in a way that maintains the bipartite property.

If, during this process, a neighbor is found to be in the same set as the current node (i.e., they have the same root), it indicates that the graph is not bipartite, and the function returns false. If no such conflict is detected after processing all nodes, the graph is bipartite, and the function returns true. This method ensures efficient and accurate detection of bipartite graphs using the Union-Find structure.

Step-by-Step Algorithm

  1. Initialization:

    • Create arrays parent and rank of size equal to the number of nodes.
    • Initialize each node as its own parent in the parent array.
    • Initialize the rank array with zeros.
  2. Iterate through each node:

    • For each node u:
      • If u has no edges, continue to the next node.
      • Find the root of node u using the find operation.
      • Select the first neighbor of u as the representative neighbor.
  3. Check neighbors and perform union operations:

    • For each neighbor v of node u:
      • If the root of u is the same as the root of v, a conflict is detected, and the graph is not bipartite. Return false.
      • Otherwise, perform a union operation between the first neighbor and the current neighbor v.
  4. Return result:

    • If no conflicts are found after processing all nodes, return true.

Algorithm Walkthrough

For the input [[1,3], [0,2], [1,3], [0,2]]:

  1. Initialization:

    • parent = [0, 1, 2, 3]
    • rank = [0, 0, 0, 0]
  2. First Node (u = 0):

    • Neighbors: [1, 3]
    • find(0): Returns 0 (root of 0 is 0)
    • First neighbor: 1
    • Neighbor (v = 1):
      • find(1): Returns 1 (root of 1 is 1)
      • Roots of 0 and 1 are different. Perform union(1, 1):
        • find(1): Returns 1
        • find(1): Returns 1
        • Roots are the same. No change in parent and rank.
    • Neighbor (v = 3):
      • find(3): Returns 3 (root of 3 is 3)
      • Roots of 0 and 3are different. Perform union(1, 3):
        • find(1): Returns 1
        • find(3): Returns 3
      • rank[1] == rank[3], so parent[3] = 1 and rank[1]++. Updated parent = [0, 1, 2, 1], rank = [0, 1, 0, 0].
  3. Second Node (u = 1):

    • Neighbors: [0, 2]
    • find(1): Returns 1 (root of 1 is 1)
    • First neighbor: 0
    • Neighbor (v = 0):
      • find(0): Returns 0 (root of 0 is 0)
      • Roots of 1 and 0 are different. Perform union(0, 0):
        • find(0): Returns 0
        • find(0): Returns 0
        • Roots are the same. No change in parent and rank.
    • Neighbor (v = 2):
      • find(2): Returns 2 (root of 2 is 2)
      • Roots are different. Perform union(0, 2):
        • find(0): Returns 0
        • find(2): Returns 2
        • rank[0] == rank[2], so parent[2] = 0 and rank[0]++. Updated parent = [0, 1, 0, 1], rank = [1, 0, 0, 0].
  4. Third Node (u = 2):

    • Neighbors: [1, 3]
    • find(2): Returns 0 (root of 2 is 0)
    • First neighbor: 1
    • Neighbor (v = 1):
      • find(1): Returns 1 (root of 1 is 1)
      • Roots are different. Perform union(1, 1):
        • find(1): Returns 1
        • find(1): Returns 1
        • Roots are the same. No change in parent and rank.
    • Neighbor (v = 3):
      • find(3): Returns 1 (root of 3 is 1)
      • Roots are different. Perform union(1, 3):
        • find(1): Returns 1
        • find(3): Returns 1
        • Roots are the same. No change in parent and rank.
  5. Fourth Node (u = 3):

    • Neighbors: [0, 2]
    • find(3): Returns 1 (root of 3 is 1)
    • First neighbor: 0
    • Neighbor (v = 0):
      • find(0): Returns 0 (root of 0 is 0)
      • Roots are different. Perform union(0, 0):
        • find(0): Returns 0
        • find(0): Returns 0
        • Roots are the same. No change in parent and rank.
    • Neighbor (v = 2):
      • find(2): Returns 0 (root of 2 is 0)
      • Roots are different. Perform union(0, 2):
        • find(0): Returns 0
        • find(2): Returns 2
        • Roots are the same. No change in parent and rank.

No conflict is detected. So, the graph is not bipartite.

Code

Python3
Python3

. . . .

Complexity Analysis

Time Complexity

    • Initializing the parent and rank arrays takes O(N) time, where N is the number of nodes in the graph.
  1. Processing each node:

    • For each node u, the code checks its neighbors.
    • Finding the parent set of each node using the Find method takes nearly constant time, O(α(N)), where α is the inverse Ackermann function, which grows very slowly.
    • Performing the union operation also takes nearly constant time, O(α(N)).
    • The nested loop iterates over all edges in the graph. If we denote the total number of edges by E, then iterating over all edges will take O(E) time.
  2. Total Time Complexity:

    • Combining all, the overall time complexity is O((N + E) * α(N)), which simplifies to O(N + E) in practical scenarios because α(N) is very small and can be considered a constant.

Space Complexity

  1. Space for parent and rank arrays:

    • The space required for the parent array is O(N).
    • The space required for the rank array is O(N).
  2. Total Space Complexity:

    • The overall space complexity is O(N), which is needed for the parent and rank arrays.

.....

.....

.....

Like the course? Get enrolled and start learning!

Table of Contents

Contents are not accessible

Contents are not accessible

Contents are not accessible

Contents are not accessible

Contents are not accessible