LeetCode: Graphs II DFS BFS Bipartite

Bipartite Algorithm Intro
Intro
A bipartite graph is a graph whose vertices can be divided into two disjoint sets such that every edge connects a vertex from one set to a vertex from the other.
No edge exists between vertices within the same set Commonly used in matching problems, scheduling, and network flows Can be checked using BFS or DFS coloring techniques
Graph Requirements
- Directed or Undirected
- Represented Using:
- Adjacency List
- Adjacency Matrix
Output
True or False whether the graph is bipartite Optionally, the two sets of vertices if it is bipartite
Video Animation
Bipartite: https://www.youtube.com/watch?v=Zg6UAnAzGGs
BFS Pseudo Code
from collections import deque
def is_bipartite(graph):
color = {}
for node in graph:
if node not in color:
queue = deque([node])
color[node] = 0 # start coloring with 0
while queue:
u = queue.popleft()
for v in graph[u]:
if v not in color:
color[v] = 1 - color[u] # alternate color
queue.append(v)
elif color[v] == color[u]:
return False # conflict detected
return TrueDFS Pseudo Code
def is_bipartite_dfs(graph):
color = {}
def dfs(node, c):
color[node] = c
for neighbor in graph[node]:
if neighbor not in color:
if not dfs(neighbor, 1 - c):
return False
elif color[neighbor] == c:
return False
return True
for node in graph:
if node not in color:
if not dfs(node, 0):
return False
return TrueTime Complexity
Each vertex is visited once Each edge is processed once
O(V + E)
Space Complexity
Color map: O(V) BFS queue: O(V) DFS recursion stack: O(V)
IRL Use Case
- Matching Problems Job assignments or tasks
- Network Flows Bipartite graphs are foundational for max flow/min cut problems
- Scheduling Two groups that must not conflict
785. Is Graph Bipartite ::1:: - Medium
Topics: Depth First Search, Breadth First Search, Union Find, Graph Theory
Intro
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 contain u). There are no parallel edges (graph[u] does not contain duplicate values). If v is in graph[u], then u is in graph[v] (the graph is undirected). The graph may not be connected, meaning there may be wo nodes u and v such 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 Input | Output |
|---|---|
| graph = [[1,2,3],[0,2],[0,1,3],[0,2]] | false |
| graph = [[1,3],[0,2],[1,3],[0,2]] | true |
Constraints:
graph.length == n
1 ≤ n ≤ 100
0 ≤ graph[u].length <
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
Abstraction
Given a graph, determine if it is bipartite
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [DFS] Two-Coloring via Depth First Search - Graph Theory/Bipartite Coloring
def isBipartite(self, graph: List[List[int]]) -> bool:
# Bipartite Rule:
# A graph is bipartite if every node can be colored one of two
# colors such that no edge ever connects two nodes of the same
# color. If we can 2-color the whole graph without conflict,
# it's bipartite. If we ever find an edge between two same-colored
# nodes, it's not.
# Two-Coloring via DFS:
# Start each uncolored node fresh, color it, then recursively
# color every neighbor the OPPOSITE color. If a neighbor is
# already colored, it must already be the opposite color --
# if it's the same color instead, that's a direct contradiction.
# Why DFS works here:
# We don't care about shortest paths or traversal order, only
# that colors alternate correctly across every edge. DFS visits
# each node once and checks all its edges, same guarantee as BFS,
# just via recursion instead of a queue.
# Disconnected Graphs:
# The graph may not be fully connected, so we must attempt to
# start a fresh coloring from every node that hasn't been
# colored yet, not just node 0.
n = len(graph)
# sc: O(n), 0 = uncolored, 1 = color A, -1 = color B
color = [0] * n
# DFS:
# colors the current node with the given color, then recurses
# into every neighbor, coloring them the opposite color.
# Returns False the moment a conflict is found.
def dfs(node, c):
# Assign:
# color current node with c
color[node] = c
# tc: O(degree(node)), visits every edge from this node once
for nei in graph[node]:
# Check: neighbor is uncolored
# Implies: this edge hasn't been checked yet
# Then: recursively color it the opposite color,
# propagate any conflict found deeper in the recursion
if color[nei] == 0:
if not dfs(nei, -c):
return False
# Check: neighbor is already colored the SAME as current
# Implies: this edge connects two same-colored nodes
# Then: bipartite coloring is impossible, conflict found
elif color[nei] == c:
return False
# No conflicts found from this node or anything below it
return True
# Components:
# graph may be disconnected, so every uncolored node needs
# its own fresh DFS start
# tc: O(n), across all starts combined, each node is only
# ever colored once, so total work stays O(n + edges)
for i in range(n):
# Check: node i is uncolored
# Implies: it belongs to a component we haven't visited yet
# Then: attempt to 2-color this whole component,
# bail out immediately if any conflict is found
if color[i] == 0:
if not dfs(i, 1):
return False
# Every component colored without conflict: bipartite
return True
# overall: tc O(n + e), n = nodes, e = total edges (sum of
# graph[u] lengths), each node colored once, each edge checked
# once from each endpoint
# overall: sc O(n), for the color array + recursion stack depthSolution 2: [Bipartite] Topological Sort using BFS - Graph Theory/Bipartite Coloring
def isBipartite(self, graph: List[List[int]]) -> bool:
# Initialization
n = len(graph) # number of nodes
color = [0] * n # 0 = uncolored, 1 = color A, -1 = color B
# Traverse all components (graph may be disconnected)
for i in range(n):
if color[i] != 0:
continue # already colored in a previous BFS
# Start BFS from uncolored node i
queue = deque([i])
color[i] = 1 # assign initial color
# BFS traversal for coloring
while queue:
node = queue.popleft()
# Explore neighbors
for nei in graph[node]:
if color[nei] == 0:
# Neighbor uncolored: assign opposite color
color[nei] = -color[node]
queue.append(nei)
elif color[nei] == color[node]:
# Neighbor already colored same as current: conflict
# Graph is not bipartite
return False
# If all nodes processed without conflict: bipartite
return True886. Possible Bipartition ::1:: - Medium
Topics: Depth First Search, Breadth First Search, Union Find, Graph Theory
Intro
We want to split a group of n people (labeled from 1 to n) into two groups of any size. Each person may dislike some other people, and they should not go into the same group. Given the integer n and the array dislikes where dislikes[i] = [ai, bi] indicates that the person labeled ai does not like the person labeled bi, return true if it is possible to split everyone into two groups in this way.
| Example Input | Output |
|---|---|
| n = 4, dislikes = [[1,2],[1,3],[2,4]] | true |
| n = 3, dislikes = [[1,2],[1,3],[2,3]] | false |
Constraints:
1 ≤ n ≤ 2000
0 ≤ dislikes.length ≤ 10^4
dislikes[i].length == 2
1 ≤ ai < bi ≤ n
All the pairs of dislikes are unique
Abstraction
Wow!
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [DFS] Two-Coloring via Depth First Search - Graph Theory/Bipartite Coloring
def possibleBipartition(self, n: int, dislikes: List[List[int]]) -> bool:
# Bipartite Rule:
# Splitting people into two groups such that no two people in
# the same group dislike each other is exactly the bipartite
# coloring problem: build an edge between every disliking pair,
# then check whether the resulting graph can be 2-colored with
# no edge connecting two same-colored nodes.
# Build Graph:
# Each dislike pair is an undirected edge -- if a dislikes b,
# b also can't be grouped with a, so the edge goes both ways.
# sc: O(n + d), d = len(dislikes), adjacency list of all people
graph = defaultdict(list)
for a, b in dislikes:
graph[a].append(b)
graph[b].append(a)
# Two-Coloring via DFS:
# Start each uncolored person fresh, color them, then recursively
# color every person they dislike the OPPOSITE color/group. If
# someone is already colored, they must already be the opposite
# color -- if it's the same color instead, that's a direct
# contradiction (two people who dislike each other, same group).
# Why DFS works here:
# We don't care about traversal order, only that colors alternate
# correctly across every disliking edge. DFS visits each person
# once and checks all their disliking edges, same guarantee as
# BFS, just via recursion instead of a queue.
# Disconnected Graphs:
# Not everyone is guaranteed to dislike (or be disliked by)
# someone else, and the dislike graph as a whole may be split
# into several separate components, so every uncolored person
# needs their own fresh DFS start.
# sc: O(n), 0 = uncolored, 1 = group A, -1 = group B
color = [0] * (n + 1)
# DFS:
# colors the current person with the given color, then recurses
# into every person they dislike, coloring them the opposite
# color. Returns False the moment a conflict is found.
def dfs(person, c):
# Assign:
# color current person with c
color[person] = c
# tc: O(degree(person)), visits every disliking edge once
for nei in graph[person]:
# Check: neighbor is uncolored
# Implies: this disliking edge hasn't been checked yet
# Then: recursively color it the opposite group,
# propagate any conflict found deeper in the recursion
if color[nei] == 0:
if not dfs(nei, -c):
return False
# Check: neighbor is already colored the SAME as current
# Implies: two people who dislike each other are stuck
# in the same group
# Then: bipartition is impossible, conflict found
elif color[nei] == c:
return False
# No conflicts found from this person or anything below them
return True
# Components:
# dislike graph may be disconnected, so every uncolored person
# needs their own fresh DFS start
# tc: O(n), across all starts combined, each person is only
# ever colored once, so total work stays O(n + d)
for person in range(1, n + 1):
# Check: person is uncolored
# Implies: they belong to a component we haven't visited yet
# Then: attempt to 2-color this whole component,
# bail out immediately if any conflict is found
if color[person] == 0:
if not dfs(person, 1):
return False
# Every component colored without conflict: valid bipartition
return True
# overall: tc O(n + d), n = people, d = len(dislikes), each
# person colored once, each disliking edge checked once from
# each endpoint
# overall: sc O(n + d), graph adjacency list + color array +
# recursion stack depthSolution 2: [Bipartite] Topological Sort using BFS - Graph Theory/Bipartite Coloring
def possibleBipartition(self, n, dislikes):
# Build graph
graph = defaultdict(list)
for a, b in dislikes:
graph[a].append(b)
graph[b].append(a)
# 0 = uncolored, 1 / -1 = two groups
color = [0] * (n + 1)
# Handle disconnected components
for person in range(1, n + 1):
if color[person] != 0:
continue
queue = deque([person])
color[person] = 1
while queue:
cur = queue.popleft()
for nei in graph[cur]:
# conflict → same color
if color[nei] == color[cur]:
return False
if color[nei] == 0:
color[nei] = -color[cur]
queue.append(nei)
return True