Graph Representation¶
Before diving into the patterns, let's briefly touch upon the two most common ways to represent a graph:
- Adjacency Matrix:
- A 2D array (list of lists in Python) where
matrix[u][v]represents the weight of the edge between nodesuandv. - For an unweighted graph, you can use a boolean matrix, where
matrix[u][v]isTrueif there is an edge andFalseotherwise.
- A 2D array (list of lists in Python) where
- Adjacency List:
- A dictionary (in Python) where each key is a node, and the value is a list of its neighbors.
- More space-efficient for sparse graphs.
Choose the representation that best suits your problem.
Graph Class in Python¶
Below is a sample Graph class implemented in Python using an adjacency list. The dictionary key is a node, and the value is a list of its neighbors.
class Graph:
def __init__(self, is_bidirectional=True):
self.adj_list = {}
self.is_bidirectional = is_bidirectional
def add_vertex(self, vertex):
if vertex not in self.adj_list:
self.adj_list[vertex] = []
def add_edge(self, source, destination):
if source not in self.adj_list:
self.add_vertex(source)
if destination not in self.adj_list:
self.add_vertex(destination)
self.adj_list[source].append(destination)
if self.is_bidirectional:
self.adj_list[destination].append(source)
def get_adj_list(self):
return self.adj_list
Graph Traversal¶
1. Depth-First Search (DFS)¶
DFS (Iterative)¶
def dfs_iterative(graph, source):
"""
Iterative DFS that prints the nodes as they are visited.
:param graph: Graph object.
:param source: Starting node for DFS.
"""
stack = [source]
visited = set()
while stack:
current_node = stack.pop()
if current_node not in visited:
visited.add(current_node)
print(current_node)
# Push all neighbors onto the stack
for neighbor in graph.get_adj_list()[current_node]:
if neighbor not in visited:
stack.append(neighbor)
DFS (Recursive)¶
def dfs_recursive(graph, source, visited=None):
"""
Recursive DFS that prints the nodes as they are visited.
:param graph: Graph object.
:param source: Starting node for DFS.
:param visited: Set to keep track of visited nodes.
"""
if visited is None:
visited = set()
visited.add(source)
print(source)
for neighbor in graph.get_adj_list()[source]:
if neighbor not in visited:
dfs_recursive(graph, neighbor, visited)
2. Breadth-First Search (BFS)¶
from collections import deque
def bfs(graph, source):
"""
BFS that prints the nodes level by level.
:param graph: Graph object.
:param source: Starting node for BFS.
"""
visited = set()
queue = deque([source])
visited.add(source)
level = 0
while queue:
level_size = len(queue)
print(f"Level {level}: ", end="")
for _ in range(level_size):
current_node = queue.popleft()
print(current_node, end=" ")
for neighbor in graph.get_adj_list()[current_node]:
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
print()
level += 1
Traversal Pattern Questions¶
1. Number of Connected Components (for an Undirected Graph)¶
def count_connected_components(graph):
"""
Counts how many connected components are in the graph.
:param graph: Graph object (undirected).
:return: Number of connected components.
"""
visited = set()
component_count = 0
for node in graph.get_adj_list().keys():
if node not in visited:
if explore_dfs(graph, node, visited):
component_count += 1
return component_count
def explore_dfs(graph, current_node, visited):
if current_node in visited:
return False
visited.add(current_node)
for neighbor in graph.get_adj_list()[current_node]:
explore_dfs(graph, neighbor, visited)
return True
2. Detect Cycle in an Undirected Graph¶
def detect_cycle_undirected(num_vertices, adjacency_list):
"""
Detect if a cycle exists in an undirected graph.
:param num_vertices: Number of vertices in the graph.
:param adjacency_list: A list of lists where adjacency_list[u] contains neighbors of u.
:return: True if cycle is found, False otherwise.
"""
visited = set()
for vertex in range(num_vertices):
if vertex not in visited:
if cycle_dfs_undirected(adjacency_list, vertex, -1, visited):
return True
return False
def cycle_dfs_undirected(adjacency_list, current_vertex, parent_vertex, visited):
visited.add(current_vertex)
for neighbor in adjacency_list[current_vertex]:
if neighbor not in visited:
if cycle_dfs_undirected(adjacency_list, neighbor, current_vertex, visited):
return True
# If neighbor is visited and not the parent, it's a cycle
elif neighbor != parent_vertex:
return True
return False
3. Detect Cycle in a Directed Graph¶
def detect_cycle_directed(num_vertices, adjacency_list):
"""
Detect cycle in a directed graph using DFS and recursion stack.
:param num_vertices: Number of vertices in the graph.
:param adjacency_list: adjacency_list[u] = list of neighbors of u.
:return: True if the directed graph has a cycle, False otherwise.
"""
visited = [False] * num_vertices
recursion_stack = [False] * num_vertices
for vertex in range(num_vertices):
if not visited[vertex]:
if cycle_dfs_directed(adjacency_list, vertex, visited, recursion_stack):
return True
return False
def cycle_dfs_directed(adjacency_list, vertex, visited, recursion_stack):
if recursion_stack[vertex]:
return True
if visited[vertex]:
return False
visited[vertex] = True
recursion_stack[vertex] = True
for neighbor in adjacency_list[vertex]:
if cycle_dfs_directed(adjacency_list, neighbor, visited, recursion_stack):
return True
recursion_stack[vertex] = False
return False
Topological Sort¶
1. Topological Sort (DFS)¶
def topological_sort_dfs(num_vertices, adjacency_list):
"""
Performs topological sort using DFS.
:param num_vertices: Number of vertices in the graph.
:param adjacency_list: adjacency_list[u] = list of neighbors of u.
:return: A list of vertices in topologically sorted order.
"""
visited = set()
result_stack = []
def dfs_topo(vertex):
if vertex in visited:
return
visited.add(vertex)
for neighbor in adjacency_list[vertex]:
if neighbor not in visited:
dfs_topo(neighbor)
# After visiting all neighbors of 'vertex', push it to the stack
result_stack.append(vertex)
for node_index in range(num_vertices):
if node_index not in visited:
dfs_topo(node_index)
# The stack has the topological order in reverse
return result_stack[::-1]
2. Topological Sort (Kahn’s Algorithm / BFS)¶
from collections import deque
def topological_sort_kahn(num_vertices, adjacency_list):
"""
Performs topological sort using Kahn's Algorithm (BFS approach).
:param num_vertices: Number of vertices.
:param adjacency_list: adjacency_list[u] = list of neighbors of u.
:return: List of vertices in topologically sorted order.
"""
in_degree = [0] * num_vertices
# Calculate in-degrees of all vertices
for u in range(num_vertices):
for v in adjacency_list[u]:
in_degree[v] += 1
# Queue for all vertices with in-degree 0
queue = deque([i for i in range(num_vertices) if in_degree[i] == 0])
topo_order = []
while queue:
current_vertex = queue.popleft()
topo_order.append(current_vertex)
for neighbor in adjacency_list[current_vertex]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
return topo_order
Grid Pattern¶
1. Number of Islands (DFS in a grid)¶
def count_islands(grid):
"""
grid: 2D list of characters 'L' (Land) or 'W' (Water).
Return: Number of islands.
"""
rows = len(grid)
cols = len(grid[0]) if rows > 0 else 0
visited = set()
island_count = 0
def explore(r, c):
# Check boundaries
if r < 0 or r >= rows or c < 0 or c >= cols:
return False
if grid[r][c] == 'W':
return False
pos_key = (r, c)
if pos_key in visited:
return False
visited.add(pos_key)
# Explore neighbors (up, down, left, right)
explore(r - 1, c)
explore(r + 1, c)
explore(r, c - 1)
explore(r, c + 1)
return True
for row_index in range(rows):
for col_index in range(cols):
if explore(row_index, col_index):
island_count += 1
return island_count
2. Rotting Oranges (BFS in a grid)¶
from collections import deque
def oranges_rotting(grid):
"""
grid: 2D list of integers:
0 - Empty cell
1 - Fresh orange
2 - Rotten orange
Return: Minimum time needed to rot all oranges, or -1 if it's impossible.
"""
rows = len(grid)
cols = len(grid[0]) if rows > 0 else 0
queue = deque()
fresh_oranges = 0
minutes_elapsed = 0
# Collect initial rotten oranges and count fresh ones
for r in range(rows):
for c in range(cols):
if grid[r][c] == 2:
queue.append((r, c))
elif grid[r][c] == 1:
fresh_oranges += 1
directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
while queue and fresh_oranges > 0:
for _ in range(len(queue)):
row_index, col_index = queue.popleft()
for dr, dc in directions:
new_r = row_index + dr
new_c = col_index + dc
# Check boundaries
if 0 <= new_r < rows and 0 <= new_c < cols and grid[new_r][new_c] == 1:
grid[new_r][new_c] = 2
queue.append((new_r, new_c))
fresh_oranges -= 1
minutes_elapsed += 1
return minutes_elapsed if fresh_oranges == 0 else -1
Shortest Path¶
1. Shortest Path in an Unweighted Graph (BFS)¶
def shortest_path_unweighted(graph, start, end):
"""
Find the shortest distance between start and end in an unweighted graph using BFS.
:param graph: Graph object.
:param start: Start node.
:param end: Destination node.
:return: Integer distance from start to end, -1 if not found.
"""
from collections import deque
queue = deque([(start, 0)])
visited = set([start])
while queue:
current_node, distance = queue.popleft()
if current_node == end:
return distance
for neighbor in graph.get_adj_list()[current_node]:
if neighbor not in visited:
visited.add(neighbor)
queue.append((neighbor, distance + 1))
return -1
2. Shortest Path in a Weighted DAG (via Topological Sort)¶
def shortest_path_weighted_dag(num_vertices, edges, source):
"""
Finds the shortest path in a weighted DAG from a single source using Topological Sort.
:param num_vertices: Number of vertices.
:param edges: List of edges [u, v, w] meaning there is an edge u->v with weight w.
:param source: Source vertex.
:return: List of shortest distances from source to each vertex; None for unreachable vertices.
"""
# Build adjacency list: graph[u] = [(v, w), (v2, w2), ...]
graph_adj = {i: [] for i in range(num_vertices)}
for u, v, w in edges:
graph_adj[u].append((v, w))
# Topological Sort using DFS
visited = set()
topo_stack = []
def dfs_topo_dag(node):
visited.add(node)
for neighbor, weight in graph_adj[node]:
if neighbor not in visited:
dfs_topo_dag(neighbor)
topo_stack.append(node)
for vertex in range(num_vertices):
if vertex not in visited:
dfs_topo_dag(vertex)
topo_order = topo_stack[::-1] # Reverse to get proper topological order
# Initialize distances
distances = [None] * num_vertices
distances[source] = 0
for node in topo_order:
if distances[node] is not None:
for neighbor, weight in graph_adj[node]:
new_distance = distances[node] + weight
if distances[neighbor] is None:
distances[neighbor] = new_distance
else:
distances[neighbor] = min(distances[neighbor], new_distance)
return distances
3. Dijkstra’s Algorithm¶
import heapq
def dijkstra(graph_adj, source, num_vertices):
"""
Dijkstra's Algorithm to find the shortest path in a weighted graph without negative weights.
:param graph_adj: Dictionary {u: [(v, w), (v2, w2)]} for edges u->v with weight w.
:param source: Source vertex.
:param num_vertices: Number of vertices.
:return: List of min distances from source to every vertex.
"""
distances = [float('inf')] * num_vertices
distances[source] = 0
# Min-heap (priority queue) of tuples (distance, node)
pq = [(0, source)]
while pq:
current_distance, current_node = heapq.heappop(pq)
# If this distance is outdated, skip it
if current_distance > distances[current_node]:
continue
for neighbor, weight in graph_adj[current_node]:
distance_through_current = distances[current_node] + weight
if distance_through_current < distances[neighbor]:
distances[neighbor] = distance_through_current
heapq.heappush(pq, (distance_through_current, neighbor))
return distances
4. Bellman-Ford Algorithm¶
def bellman_ford(num_vertices, edges, source):
"""
Bellman-Ford Algorithm to detect negative cycles and find shortest paths.
:param num_vertices: Number of vertices.
:param edges: List of edges [u, v, weight].
:param source: Source vertex.
:return: List of distances; [-1] if a negative cycle is detected.
"""
INF = 10**9
distances = [INF] * num_vertices
distances[source] = 0
# Relax edges (num_vertices - 1) times
for _ in range(num_vertices - 1):
for u, v, w in edges:
if distances[u] != INF and distances[u] + w < distances[v]:
distances[v] = distances[u] + w
# Check for negative-weight cycles
for u, v, w in edges:
if distances[u] != INF and distances[u] + w < distances[v]:
return [-1] # Indicates negative cycle
return distances
5. Floyd-Warshall Algorithm¶
def floyd_warshall(matrix):
"""
Floyd Warshall Algorithm for all-pairs shortest paths.
matrix: 2D list (matrix[u][v]) with distances; -1 indicates no direct edge.
Updates the matrix in-place with shortest distances, -1 if unreachable.
"""
n = len(matrix)
INF = 10**9
# Replace -1 with INF, and 0 on the diagonal
for i in range(n):
for j in range(n):
if matrix[i][j] == -1 and i != j:
matrix[i][j] = INF
if i == j:
matrix[i][j] = 0
for k in range(n):
for i in range(n):
for j in range(n):
if matrix[i][k] + matrix[k][j] < matrix[i][j]:
matrix[i][j] = matrix[i][k] + matrix[k][j]
# Convert INF back to -1
for i in range(n):
for j in range(n):
if matrix[i][j] == INF:
matrix[i][j] = -1
Union-Find (Disjoint Set Union - DSU)¶
class DSU:
def __init__(self, n):
"""
:param n: Number of elements (0 to n-1).
"""
self.parent = [i for i in range(n)]
self.rank = [0] * n
self.size = [1] * n
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union_by_rank(self, x, y):
"""
Union two sets by rank.
:return: True if merged, False if already in the same set.
"""
root_x = self.find(x)
root_y = self.find(y)
if root_x == root_y:
return False
if self.rank[root_x] < self.rank[root_y]:
self.parent[root_x] = root_y
elif self.rank[root_x] > self.rank[root_y]:
self.parent[root_y] = root_x
else:
self.parent[root_y] = root_x
self.rank[root_x] += 1
return True
def union_by_size(self, x, y):
"""
Union two sets by size.
:return: True if merged, False if already in the same set.
"""
root_x = self.find(x)
root_y = self.find(y)
if root_x == root_y:
return False
if self.size[root_x] < self.size[root_y]:
self.parent[root_x] = root_y
self.size[root_y] += self.size[root_x]
else:
self.parent[root_y] = root_x
self.size[root_x] += self.size[root_y]
return True
Minimum Spanning Tree (MST)¶
1. Prim’s Algorithm¶
import heapq
def mst_prim(adjacency_dict, num_vertices):
"""
Prim's Algorithm to find the cost of MST in a weighted undirected graph.
:param adjacency_dict: {node: [(neighbor, weight), ...], ...}
:param num_vertices: Total number of vertices.
:return: The total cost of the MST.
"""
visited = [False] * num_vertices
min_heap = [(0, 0)] # (cost, node) - start from node 0
total_cost = 0
edges_used = 0
while min_heap and edges_used < num_vertices:
cost, node = heapq.heappop(min_heap)
if visited[node]:
continue
visited[node] = True
total_cost += cost
edges_used += 1
for neighbor, weight in adjacency_dict[node]:
if not visited[neighbor]:
heapq.heappush(min_heap, (weight, neighbor))
return total_cost
2. Kruskal’s Algorithm¶
def mst_kruskal(num_vertices, edges):
"""
Kruskal's Algorithm to find the MST cost in a weighted undirected graph.
:param num_vertices: Number of vertices.
:param edges: List of edges [u, v, weight].
:return: The total cost of the MST.
"""
# Sort edges by weight
edges.sort(key=lambda x: x[2])
dsu = DSU(num_vertices)
mst_cost = 0
edges_used = 0
for u, v, weight in edges:
if dsu.union_by_rank(u, v):
mst_cost += weight
edges_used += 1
if edges_used == num_vertices - 1:
break
return mst_cost
Kosaraju’s Algorithm¶
Number of Strongly Connected Components (SCCs)¶
def kosaraju_scc(num_vertices, adjacency_list):
"""
Kosaraju's Algorithm to find the number of strongly connected components.
:param num_vertices: Number of vertices.
:param adjacency_list: adjacency_list[u] = list of neighbors of u.
:return: The count of strongly connected components.
"""
visited = [False] * num_vertices
stack = []
# 1. Fill vertices in stack by finish time
def dfs_fill(vertex):
visited[vertex] = True
for neighbor in adjacency_list[vertex]:
if not visited[neighbor]:
dfs_fill(neighbor)
stack.append(vertex)
for vertex in range(num_vertices):
if not visited[vertex]:
dfs_fill(vertex)
# 2. Create a reversed graph
reversed_adj = [[] for _ in range(num_vertices)]
for u in range(num_vertices):
for v in adjacency_list[u]:
reversed_adj[v].append(u)
# 3. Pop from stack, do DFS on reversed graph
visited = [False] * num_vertices
scc_count = 0
def dfs_reverse(vertex):
visited[vertex] = True
for neighbor in reversed_adj[vertex]:
if not visited[neighbor]:
dfs_reverse(neighbor)
while stack:
node = stack.pop()
if not visited[node]:
dfs_reverse(node)
scc_count += 1
return scc_count
Summary¶
This Python Cheatsheet covers a variety of graph algorithms and patterns:
- Traversal: DFS (iterative & recursive), BFS
- Connectivity: Counting connected components
- Cycle Detection: Directed & Undirected Graphs
- Topological Sort: DFS method & Kahn’s Algorithm
- Grid-based BFS/DFS: Common patterns like “Number of Islands” and “Rotting Oranges”
- Shortest Path: BFS (unweighted), Dijkstra’s, Bellman-Ford, Floyd-Warshall, and Weighted DAG via Topological Sort
- Disjoint Set (Union-Find): For advanced operations like Kruskal's MST
- Minimum Spanning Tree: Prim’s and Kruskal’s
- Kosaraju’s Algorithm: Strongly Connected Components
Note: The complexity and specific implementations can vary based on your use case and constraints. Understanding the fundamentals and customizing them to fit your needs is key.
Happy Coding!