886. Possible Bipartition
Problem Description
You have n
people labeled from 1
to n
that need to be divided into exactly two groups. Some people dislike each other and cannot be placed in the same group.
You're given:
- An integer
n
representing the number of people - An array
dislikes
where each elementdislikes[i] = [aα΅’, bα΅’]
means personaα΅’
dislikes personbα΅’
(and vice versa)
Your task is to determine if it's possible to split all n
people into two groups such that no two people who dislike each other are in the same group. Return true
if such a division is possible, false
otherwise.
The problem is essentially asking whether you can create a valid bipartition of people based on their dislike relationships. This is equivalent to checking if the graph formed by the dislike relationships is bipartite - meaning the graph can be colored with two colors such that no two adjacent nodes (people who dislike each other) have the same color.
The solution uses depth-first search (DFS) with graph coloring. It builds an adjacency list from the dislikes array, then attempts to color each connected component with alternating colors (represented as 1
and 2
). If at any point two people who dislike each other would need the same color, the bipartition is impossible. The code converts 1-indexed people to 0-indexed for easier array manipulation and uses 3 - c
to alternate between colors 1
and 2
.
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 involves people (nodes) and their dislike relationships (edges). We need to model this as a graph where each person is a node and each dislike relationship creates an edge between two nodes.
Is it a tree?
- No: The graph is not necessarily a tree. It can have cycles (e.g., person A dislikes B, B dislikes C, and C dislikes A), and it may have multiple disconnected components.
Is the problem related to directed acyclic graphs?
- No: The dislike relationships are bidirectional (if A dislikes B, then B dislikes A), making this an undirected graph. Also, the graph may contain cycles.
Is the problem related to shortest paths?
- No: We're not trying to find the shortest path between nodes. Instead, we need to determine if we can partition the nodes into two groups.
Does the problem involve connectivity?
- Yes: The problem involves checking if we can properly color/partition the graph. This is essentially a bipartite graph detection problem, which requires exploring the connectivity and relationships between nodes.
Does the problem have small constraints?
- Yes: With constraints typically up to
n = 2000
and a limited number of dislikes, we can use DFS to explore each connected component.
DFS/backtracking?
- Yes: DFS is perfect for this problem. We can use DFS to traverse each connected component and attempt to color nodes with alternating colors (representing the two groups).
Conclusion: The flowchart suggests using DFS to solve this bipartite graph detection problem. We traverse the graph using DFS, assigning alternating colors to connected nodes, and check if any conflicts arise (two connected nodes having the same color).
Intuition
The key insight is recognizing that this is a graph bipartition problem. If we think of people as nodes and dislikes as edges, we need to color the graph with exactly two colors such that no adjacent nodes share the same color.
Why does this work? Consider what happens when we try to split people into two groups:
- Each person must go into one of two groups (let's call them Group 1 and Group 2)
- If person A dislikes person B, they must be in different groups
- This is exactly like coloring a graph with two colors where adjacent nodes must have different colors
The critical realization is that a graph can be 2-colored (bipartite) if and only if it contains no odd-length cycles. Why? In any cycle, if we alternate colors around it, an odd-length cycle would force us to give the same color to two adjacent nodes, creating a conflict.
Our approach uses DFS to traverse each connected component:
- Start with any uncolored person and assign them to Group 1 (color
1
) - All their disliked people must go to Group 2 (color
2
) - All people disliked by Group 2 members must go back to Group 1
- Continue this alternating pattern
If at any point we try to assign a color to someone who already has a different color assigned, we've found a conflict - the graph isn't bipartite, and we can't split the people into two valid groups.
The beauty of this solution is that DFS naturally handles disconnected components. Some people might not dislike anyone or be disliked by anyone - they form separate components that we can color independently. The all(c or dfs(i, 1) for i, c in enumerate(color))
ensures we check every component, starting a new DFS whenever we find an uncolored node.
Learn more about Depth-First Search, Breadth-First Search, Union Find and Graph patterns.
Solution Approach
The implementation uses DFS with graph coloring to detect if the graph formed by dislike relationships is bipartite.
Data Structures:
g
: An adjacency list (usingdefaultdict(list)
) to represent the undirected graph of dislikescolor
: An array wherecolor[i]
stores the color (group) of personi
(0 = uncolored, 1 = Group 1, 2 = Group 2)
Building the Graph:
for a, b in dislikes: a, b = a - 1, b - 1 # Convert to 0-indexed g[a].append(b) g[b].append(a)
Since dislikes are mutual, we add edges in both directions to create an undirected graph.
DFS Coloring Function:
def dfs(i, c):
color[i] = c
for j in g[i]:
if color[j] == c:
return False
if color[j] == 0 and not dfs(j, 3 - c):
return False
return True
The DFS function attempts to color person i
with color c
:
- Assign color
c
to personi
- For each person
j
that personi
dislikes:- If
j
already has the same colorc
, we have a conflict β returnFalse
- If
j
is uncolored (0), recursively color it with the opposite color3 - c
(alternates between 1 and 2) - If the recursive coloring fails, propagate the failure
- If
Main Logic:
return all(c or dfs(i, 1) for i, c in enumerate(color))
This line handles multiple disconnected components:
- For each person
i
, check if they're already colored (c
is non-zero) - If uncolored, start a new DFS from that person with color 1
- The expression
c or dfs(i, 1)
returnsTrue
if either:- Person is already colored (part of a previously processed component)
- Starting a new DFS from this person succeeds
- Using
all()
ensures every component can be properly 2-colored
Time Complexity: O(V + E) where V is the number of people and E is the number of dislike relationships Space Complexity: O(V + E) for the adjacency list and color array
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with n = 5
people and dislikes = [[1,2], [2,3], [3,4], [4,5], [1,5]]
.
Step 1: Build the graph After converting to 0-indexed and creating the adjacency list:
- Person 0 dislikes: [1, 4]
- Person 1 dislikes: [0, 2]
- Person 2 dislikes: [1, 3]
- Person 3 dislikes: [2, 4]
- Person 4 dislikes: [3, 0]
Step 2: Initialize color array
color = [0, 0, 0, 0, 0]
(all uncolored)
Step 3: Start DFS from person 0
- Color person 0 with color 1:
color = [1, 0, 0, 0, 0]
- Person 0 dislikes persons 1 and 4, so we need to color them with 2
Step 4: DFS to person 1
- Color person 1 with color 2:
color = [1, 2, 0, 0, 0]
- Person 1 dislikes person 0 (already colored 1, different from 2 β) and person 2
- DFS to person 2 with color 1
Step 5: DFS to person 2
- Color person 2 with color 1:
color = [1, 2, 1, 0, 0]
- Person 2 dislikes person 1 (already colored 2, different from 1 β) and person 3
- DFS to person 3 with color 2
Step 6: DFS to person 3
- Color person 3 with color 2:
color = [1, 2, 1, 2, 0]
- Person 3 dislikes person 2 (already colored 1, different from 2 β) and person 4
- DFS to person 4 with color 1
Step 7: DFS to person 4
- Color person 4 with color 1:
color = [1, 2, 1, 2, 1]
- Person 4 dislikes person 3 (already colored 2, different from 1 β) and person 0
- Check person 0: it has color 1, same as person 4's color 1 β
Conflict detected! Person 4 and person 0 dislike each other but would need the same color. This forms an odd cycle (0β1β2β3β4β0) of length 5, making bipartition impossible.
The function returns False
- we cannot split these 5 people into two groups.
Counter-example that works:
If we had dislikes = [[1,2], [2,3], [3,4]]
with n = 4
:
- DFS colors: person 0 (color 1), person 1 (color 2), person 2 (color 1), person 3 (color 2)
- Final groups: Group 1 = {1, 3}, Group 2 = {2, 4}
- No conflicts, returns
True
Solution Implementation
1from collections import defaultdict
2from typing import List
3
4class Solution:
5 def possibleBipartition(self, n: int, dislikes: List[List[int]]) -> bool:
6 """
7 Determine if we can split n people into two groups such that
8 no two people who dislike each other are in the same group.
9
10 This is a graph bipartition problem - we need to check if the graph
11 formed by dislike relationships is bipartite.
12
13 Args:
14 n: Number of people (labeled from 1 to n)
15 dislikes: List of pairs [a, b] indicating person a and b dislike each other
16
17 Returns:
18 True if bipartition is possible, False otherwise
19 """
20
21 def dfs(node: int, assigned_color: int) -> bool:
22 """
23 Depth-first search to color the graph with two colors.
24
25 Args:
26 node: Current node to color
27 assigned_color: Color to assign (1 or 2)
28
29 Returns:
30 True if valid coloring is possible, False otherwise
31 """
32 # Assign color to current node
33 colors[node] = assigned_color
34
35 # Check all neighbors
36 for neighbor in adjacency_list[node]:
37 # If neighbor has same color, graph is not bipartite
38 if colors[neighbor] == assigned_color:
39 return False
40
41 # If neighbor is uncolored, recursively color it with opposite color
42 # Use 3 - assigned_color to alternate between colors 1 and 2
43 if colors[neighbor] == 0 and not dfs(neighbor, 3 - assigned_color):
44 return False
45
46 return True
47
48 # Build adjacency list for undirected graph
49 adjacency_list = defaultdict(list)
50
51 # Initialize colors array (0 = uncolored, 1 = color1, 2 = color2)
52 colors = [0] * n
53
54 # Convert to 0-indexed and build adjacency list
55 for person_a, person_b in dislikes:
56 person_a -= 1 # Convert to 0-indexed
57 person_b -= 1 # Convert to 0-indexed
58 adjacency_list[person_a].append(person_b)
59 adjacency_list[person_b].append(person_a)
60
61 # Try to color each connected component
62 # If node is already colored (color != 0), skip it
63 # Otherwise, start DFS coloring from this node with color 1
64 return all(color != 0 or dfs(node_idx, 1)
65 for node_idx, color in enumerate(colors))
66
1class Solution {
2 // Adjacency list to represent the graph
3 private List<Integer>[] graph;
4 // Array to store color assignments (0: unvisited, 1: color1, 2: color2)
5 private int[] colors;
6
7 public boolean possibleBipartition(int n, int[][] dislikes) {
8 // Initialize graph as an array of lists
9 graph = new List[n];
10 colors = new int[n];
11
12 // Create empty adjacency lists for each node
13 Arrays.setAll(graph, index -> new ArrayList<>());
14
15 // Build undirected graph from dislikes array
16 // Convert 1-indexed to 0-indexed
17 for (int[] edge : dislikes) {
18 int nodeA = edge[0] - 1;
19 int nodeB = edge[1] - 1;
20 graph[nodeA].add(nodeB);
21 graph[nodeB].add(nodeA);
22 }
23
24 // Try to color each connected component
25 for (int node = 0; node < n; ++node) {
26 // If node is unvisited, start DFS coloring from this node
27 if (colors[node] == 0) {
28 // Start coloring with color 1
29 if (!dfs(node, 1)) {
30 return false;
31 }
32 }
33 }
34
35 return true;
36 }
37
38 /**
39 * Performs DFS to check if graph can be bipartitioned
40 * @param currentNode - the current node being visited
41 * @param currentColor - the color to assign to current node (1 or 2)
42 * @return true if bipartition is possible, false otherwise
43 */
44 private boolean dfs(int currentNode, int currentColor) {
45 // Assign color to current node
46 colors[currentNode] = currentColor;
47
48 // Check all neighbors
49 for (int neighbor : graph[currentNode]) {
50 // If neighbor has same color as current node, not bipartite
51 if (colors[neighbor] == currentColor) {
52 return false;
53 }
54
55 // If neighbor is unvisited, recursively color it with opposite color
56 // Opposite color: if current is 1, next is 2; if current is 2, next is 1
57 // Formula: 3 - currentColor gives the opposite color
58 if (colors[neighbor] == 0 && !dfs(neighbor, 3 - currentColor)) {
59 return false;
60 }
61 }
62
63 return true;
64 }
65}
66
1class Solution {
2public:
3 bool possibleBipartition(int n, vector<vector<int>>& dislikes) {
4 // Build adjacency list for the graph
5 // Key: person index, Value: list of people they dislike
6 unordered_map<int, vector<int>> graph;
7
8 // Convert dislikes into bidirectional graph edges
9 // Note: Converting from 1-indexed to 0-indexed
10 for (auto& edge : dislikes) {
11 int person1 = edge[0] - 1;
12 int person2 = edge[1] - 1;
13 graph[person1].push_back(person2);
14 graph[person2].push_back(person1);
15 }
16
17 // Color array to track which group each person belongs to
18 // 0: unvisited, 1: group 1, 2: group 2
19 vector<int> colors(n, 0);
20
21 // DFS function to color the graph with two colors
22 // Returns true if bipartition is possible from current node
23 function<bool(int, int)> dfs = [&](int currentNode, int currentColor) -> bool {
24 // Assign color to current node
25 colors[currentNode] = currentColor;
26
27 // Check all neighbors (people that current person dislikes)
28 for (int neighbor : graph[currentNode]) {
29 // If neighbor is unvisited, recursively color it with opposite color
30 // Using 3 - currentColor to alternate between 1 and 2
31 if (colors[neighbor] == 0) {
32 if (!dfs(neighbor, 3 - currentColor)) {
33 return false;
34 }
35 }
36 // If neighbor has same color as current node, bipartition is impossible
37 else if (colors[neighbor] == currentColor) {
38 return false;
39 }
40 }
41 return true;
42 };
43
44 // Process all connected components in the graph
45 for (int person = 0; person < n; ++person) {
46 // Start DFS from unvisited nodes with color 1
47 if (colors[person] == 0) {
48 if (!dfs(person, 1)) {
49 return false;
50 }
51 }
52 }
53
54 // All nodes successfully colored with two colors
55 return true;
56 }
57};
58
1/**
2 * Determines if it's possible to split n people into two groups where no two people who dislike each other are in the same group.
3 * This is essentially a bipartite graph problem where we need to check if the graph can be 2-colored.
4 *
5 * @param n - The number of people (labeled from 1 to n)
6 * @param dislikes - Array of pairs where dislikes[i] = [a, b] means person a and person b dislike each other
7 * @returns true if possible to split into two groups, false otherwise
8 */
9function possibleBipartition(n: number, dislikes: number[][]): boolean {
10 // Color array to track which group each person belongs to (0: unvisited, 1: group 1, 2: group 2)
11 const colors: number[] = new Array(n + 1).fill(0);
12
13 // Adjacency list representation of the graph
14 const adjacencyList: number[][] = Array.from({ length: n + 1 }, () => []);
15
16 /**
17 * Depth-first search to color the graph
18 * Attempts to color node and all its neighbors with alternating colors
19 *
20 * @param node - Current node to color
21 * @param color - Color to assign to current node (1 or 2)
22 * @returns true if conflict detected (same color for adjacent nodes), false otherwise
23 */
24 const hasConflict = (node: number, color: number): boolean => {
25 // Assign color to current node
26 colors[node] = color;
27
28 // Check all neighbors
29 for (const neighbor of adjacencyList[node]) {
30 // If neighbor has same color as current node, we have a conflict
31 if (colors[neighbor] === colors[node]) {
32 return true;
33 }
34
35 // If neighbor is unvisited, recursively color it with opposite color
36 // Using XOR with 3 to flip between 1 and 2: 3^1=2, 3^2=1
37 if (colors[neighbor] === 0 && hasConflict(neighbor, 3 ^ color)) {
38 return true;
39 }
40 }
41
42 return false;
43 };
44
45 // Build the undirected graph from dislikes array
46 for (const [personA, personB] of dislikes) {
47 adjacencyList[personA].push(personB);
48 adjacencyList[personB].push(personA);
49 }
50
51 // Try to color each connected component
52 for (let person = 1; person <= n; person++) {
53 // If person is unvisited, start coloring from this node
54 if (colors[person] === 0 && hasConflict(person, 1)) {
55 // If we find a conflict while coloring, bipartition is not possible
56 return false;
57 }
58 }
59
60 // All nodes colored successfully without conflicts
61 return true;
62}
63
Time and Space Complexity
Time Complexity: O(V + E)
where V = n
is the number of people and E
is the number of dislike relationships.
- Building the adjacency list takes
O(E)
time as we iterate through all dislike pairs once - The DFS traversal visits each vertex at most once and explores each edge at most twice (once from each endpoint)
- For each vertex, we process all its adjacent vertices, which in total across all vertices is
O(E)
- The
all()
function with the generator expression triggers DFS for each unvisited component, but the total work done across all DFS calls is still bounded byO(V + E)
Space Complexity: O(V + E)
- The adjacency list
g
stores all edges, requiringO(E)
space (each edge is stored twice in an undirected graph) - The
color
array usesO(V)
space to track the coloring of each vertex - The recursive DFS call stack can go as deep as
O(V)
in the worst case (when the graph forms a long path) - Total space complexity is
O(V + E)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to Convert Between 1-indexed and 0-indexed
One of the most common mistakes is forgetting that the problem uses 1-indexed people (labeled from 1 to n) while arrays in most programming languages are 0-indexed.
Incorrect approach:
# Forgetting to convert indices for person_a, person_b in dislikes: adjacency_list[person_a].append(person_b) # Wrong! Using 1-indexed directly adjacency_list[person_b].append(person_a) colors = [0] * n # Later accessing colors[person_a] where person_a could be n, causing IndexError
Solution: Always convert to 0-indexed immediately when processing the input:
for person_a, person_b in dislikes: person_a -= 1 # Convert to 0-indexed person_b -= 1 adjacency_list[person_a].append(person_b) adjacency_list[person_b].append(person_a)
2. Not Handling Disconnected Components
Another critical mistake is assuming all people form a single connected graph. If there are isolated groups of people with no dislikes between groups, you need to check each component separately.
Incorrect approach:
# Only checking from node 0
def possibleBipartition(self, n: int, dislikes: List[List[int]]) -> bool:
# ... build graph ...
if not dislikes:
return True
return dfs(0, 1) # Wrong! Only checks component containing person 0
Solution: Iterate through all nodes and start a new DFS for each uncolored node:
# Check all nodes, starting new DFS for uncolored ones
return all(color != 0 or dfs(node_idx, 1)
for node_idx, color in enumerate(colors))
3. Using Wrong Color Alternation Logic
When alternating between two colors/groups, using incorrect logic can lead to assigning the same color to adjacent nodes.
Incorrect approaches:
# Wrong: This doesn't alternate properly dfs(neighbor, assigned_color + 1) # Could give color 3, 4, etc. # Wrong: Using modulo incorrectly dfs(neighbor, assigned_color % 2) # Color 1 β 1, Color 2 β 0 (invalid)
Solution:
Use 3 - assigned_color
to alternate between colors 1 and 2:
dfs(neighbor, 3 - assigned_color) # Color 1 β 2, Color 2 β 1
4. Not Checking Already Colored Nodes Properly
Failing to handle the case where a neighbor is already colored (but with a valid different color) can cause unnecessary recursion or incorrect results.
Incorrect approach:
def dfs(node, assigned_color):
colors[node] = assigned_color
for neighbor in adjacency_list[node]:
if colors[neighbor] == assigned_color:
return False
# Wrong: Recursing even when neighbor is already correctly colored
if not dfs(neighbor, 3 - assigned_color):
return False
return True
Solution: Only recurse on uncolored neighbors:
def dfs(node, assigned_color):
colors[node] = assigned_color
for neighbor in adjacency_list[node]:
if colors[neighbor] == assigned_color:
return False
# Only recurse if neighbor is uncolored
if colors[neighbor] == 0 and not dfs(neighbor, 3 - assigned_color):
return False
return True
What is the best way of checking if an element exists in a sorted array once in terms of time complexity? Select the best that applies.
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!