Skip to content

Advanced Graph Algorithms

Finding pockets of mutual reachability: Strongly connected components are like friend groups where everyone knows everyone, within an SCC, you can reach any node from any other node. Condensing SCCs into single nodes reveals the DAG structure of the graph.

Why it matters: SCCs are fundamental to understanding graph structure, they identify cyclic dependencies in build systems, strongly connected regions in social networks, and mutually reachable states in automata.

The key insight: Kosaraju algorithm uses two DFS passes, the first determines the order to process nodes, the second finds SCCs on the transposed graph. This elegant two-pass approach runs in O(V+E) time.

A strongly connected component (SCC) of a directed graph is a maximal set of vertices such that there is a path from every vertex to every other vertex within the set.

Kosaraju’s algorithm finds all SCCs in O(V+E)O(V + E) time using two DFS passes.

def kosaraju(n, adj):
"""
Find strongly connected components using Kosaraju's algorithm.
Time: O(V + E)
Space: O(V + E)
Returns: list of SCCs (each SCC is a list of vertices)
"""
visited = [False] * n
order = []
def dfs1(v):
visited[v] = True
for u in adj[v]:
if not visited[u]:
dfs1(u)
order.append(v)
for v in range(n):
if not visited[v]:
dfs1(v)
radj = [[] for _ in range(n)]
for v in range(n):
for u in adj[v]:
radj[u].append(v)
visited = [False] * n
sccs = []
def dfs2(v, component):
visited[v] = True
component.append(v)
for u in radj[v]:
if not visited[u]:
dfs2(u, component)
for v in reversed(order):
if not visited[v]:
component = []
dfs2(v, component)
sccs.append(component)
return sccs

Tarjan’s algorithm finds SCCs in a single DFS pass using a stack and low-link values.

def tarjan_scc(n, adj):
"""
Find SCCs using Tarjan's algorithm.
Time: O(V + E)
Space: O(V)
"""
index_counter = [0]
stack = []
on_stack = [False] * n
index = [-1] * n
low = [0] * n
sccs = []
def strongconnect(v):
index[v] = index_counter[0]
low[v] = index_counter[0]
index_counter[0] += 1
stack.append(v)
on_stack[v] = True
for w in adj[v]:
if index[w] == -1:
strongconnect(w)
low[v] = min(low[v], low[w])
elif on_stack[w]:
low[v] = min(low[v], index[w])
if low[v] == index[v]:
scc = []
while True:
w = stack.pop()
on_stack[w] = False
scc.append(w)
if w == v:
break
sccs.append(scc)
for v in range(n):
if index[v] == -1:
strongconnect(v)
return sccs

The condensation of a directed graph is the DAG formed by contracting each SCC to a single vertex. The condensation DAG is useful for solving problems on the original graph.

def build_condensation(n, adj, sccs):
"""
Build condensation DAG from SCCs.
Time: O(V + E)
Space: O(V + E)
"""
scc_id = [0] * n
for i, scc in enumerate(sccs):
for v in scc:
scc_id[v] = i
num_sccs = len(sccs)
cond_adj = [set() for _ in range(num_sccs)]
for v in range(n):
for u in adj[v]:
if scc_id[v] != scc_id[u]:
cond_adj[scc_id[v]].add(scc_id[u])
return [list(s) for s in cond_adj], scc_id
graph TD
    subgraph "Original Graph"
        A1["A"] --> B1["B"]
        B1 --> C1["C"]
        C1 --> A1
        C1 --> D1["D"]
        D1 --> E1["E"]
        E1 --> F1["F"]
        F1 --> D1
        F1 --> G1["G"]
    end

    subgraph "Condensation DAG"
        SCC1["SCC1: A,B,C"] --> SCC2["SCC2: D,E,F"]
        SCC2 --> SCC3["SCC3: G"]
    end

A bridge is an edge whose removal increases the number of connected components.

def find_bridges(n, adj):
"""
Find all bridges in an undirected graph.
Time: O(V + E)
Space: O(V)
Returns: list of (u, v) bridges
"""
visited = [False] * n
tin = [0] * n
low = [0] * n
timer = [0]
bridges = []
def dfs(v, parent):
visited[v] = True
tin[v] = low[v] = timer[0]
timer[0] += 1
for u in adj[v]:
if u == parent:
continue
if visited[u]:
low[v] = min(low[v], tin[u])
else:
dfs(u, v)
low[v] = min(low[v], low[u])
if low[u] > tin[v]:
bridges.append((v, u))
for v in range(n):
if not visited[v]:
dfs(v, -1)
return bridges

An articulation point is a vertex whose removal increases the number of connected components.

def find_articulation_points(n, adj):
"""
Find all articulation points in an undirected graph.
Time: O(V + E)
Space: O(V)
"""
visited = [False] * n
tin = [0] * n
low = [0] * n
timer = [0]
is_articulation = [False] * n
def dfs(v, parent):
visited[v] = True
tin[v] = low[v] = timer[0]
timer[0] += 1
children = 0
for u in adj[v]:
if u == parent:
continue
if visited[u]:
low[v] = min(low[v], tin[u])
else:
dfs(u, v)
low[v] = min(low[v], low[u])
if low[u] >= tin[v] and parent != -1:
is_articulation[v] = True
children += 1
if parent == -1 and children > 1:
is_articulation[v] = True
for v in range(n):
if not visited[v]:
dfs(v, -1)
return [v for v in range(n) if is_articulation[v]]
  • Graph Algorithms — Dijkstra’s and MST algorithms provide the foundation for the advanced techniques here.
  • Dynamic Programming — Floyd-Warshall is a classic all-pairs shortest path algorithm using dynamic programming.
  • Binary Search Trees — Priority queues used in Dijkstra’s algorithm are implemented using heap-based trees.
  • Deques and Priority Queues — Priority queues are essential for efficient implementations of shortest path algorithms.