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.