Skip to content

Graph Algorithms

Finding the best path through a maze: Graph algorithms are like navigating a city — Dijkstra finds the fastest route, BFS finds the nearest hospital, and minimum spanning trees connect all buildings with the least road construction. Each algorithm answers a different “best path” question.

Why it matters: Graph algorithms power GPS navigation, social network analysis, network routing, and countless other applications. Understanding them lets you solve problems involving relationships and connectivity efficiently.

The key insight: Dijkstra greedy strategy works because shortest paths have optimal substructure — the shortest path from A to C through B is the shortest path from A to B plus the shortest path from B to C.

Dijkstra’s algorithm finds the shortest path from a single source to all other vertices in a graph With non-negative edge weights. It uses a greedy strategy: always process the unvisited vertex with The smallest known distance.

import heapq
from collections import defaultdict, deque
def dijkstra(graph, source):
"""
Dijkstra's shortest paths from source to all vertices.
Requires: all edge weights >= 0
Time: O((V + E) log V) with binary heap
O(V^2) with naive array (better for dense graphs)
Space: O(V)
"""
dist = {v: float('inf') for v in graph}
dist[source] = 0
prev = {v: None for v in graph}
visited = set()
pq = [(0, source)] # (distance, vertex)
while pq:
d, u = heapq.heappop(pq)
if u in visited:
continue
visited.add(u)
for v, w in graph[u]:
if v in visited:
continue
new_dist = d + w
if new_dist < dist[v]:
dist[v] = new_dist
prev[v] = u
heapq.heappush(pq, (new_dist, v))
return dist, prev
def reconstruct_path(prev, target):
"""Reconstruct shortest path from predecessors array."""
path = []
current = target
while current is not None:
path.append(current)
current = prev[current]
return path[::-1]

Why Dijkstra fails with negative edges: Dijkstra’s greedy choice assumes that once a vertex is Processed, its distance is final. With negative edges, a shorter path may be discovered later Through a vertex that has already been processed.

Graph TypeDijkstraBellman-FordFloyd-Warshall
Non-negative, single sourceO((V+E)logV)O((V+E) \log V)O(VE)O(VE)O(V3)O(V^3)
Negative, no negative cyclesFailsO(VE)O(VE)O(V3)O(V^3)
Negative cycle detectionCannotYesYes
All-pairsRun VV times: O(V(V+E)logV)O(V(V+E)\log V)Run VV times: O(V2E)O(V^2 E)O(V3)O(V^3)

Bellman-Ford handles negative edge weights and detects negative cycles. It relaxes all edges V1V - 1 Times; if a further relaxation is possible, a negative cycle exists.

def bellman_ford(vertices, edges, source):
"""
Bellman-Ford shortest paths from source.
Handles negative edges, detects negative cycles.
Time: O(V * E)
Space: O(V)
"""
dist = {v: float('inf') for v in vertices}
dist[source] = 0
prev = {v: None for v in vertices}
# Relax all edges V-1 times
for _ in range(len(vertices) - 1):
updated = False
for u, v, w in edges:
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
prev[v] = u
updated = True
if not updated:
break # early termination
# Check for negative cycles
for u, v, w in edges:
if dist[u] + w < dist[v]:
raise ValueError("Negative cycle detected")
return dist, prev

Negative cycle detection: If after V1V-1 relaxations, any edge can still be relaxed, there Exists a reachable negative cycle from the source. This is used in routing protocols (RIP) and Arbitrage detection in currency exchange.

def detect_arbitrage(currencies, rates):
"""
Detect arbitrage opportunity in currency exchange.
Uses Bellman-Ford on the log of exchange rates.
Time: O(V * E) where V = currencies, E = V^2
"""
vertices = list(currencies)
edges = []
for i, c1 in enumerate(vertices):
for j, c2 in enumerate(vertices):
if i != j:
# Edge weight = -log(rate): shorter path = better exchange rate
edges.append((i, j, -rates[c1][c2]))
try:
bellman_ford(range(len(vertices)), edges, 0)
return False # no arbitrage
except ValueError:
return True # negative cycle = arbitrage opportunity

Floyd-Warshall computes shortest paths between all pairs of vertices. It works with negative Edges (but not negative cycles).

dp[k][i][j]=min(dp[k1][i][j],dp[k1][i][k]+dp[k1][k][j])dp[k][i][j] = \min(dp[k-1][i][j], dp[k-1][i][k] + dp[k-1][k][j])

In practice, we use only a 2D table because dp[k]dp[k] only depends on dp[k1]dp[k-1].

def floyd_warshall(n, edges):
"""
All-pairs shortest paths.
Time: O(V^3), Space: O(V^2)
"""
INF = float('inf')
dist = [[INF] * n for _ in range(n)]
for i in range(n):
dist[i][i] = 0
for u, v, w in edges:
dist[u][v] = min(dist[u][v], w)
for k in range(n):
for i in range(n):
for j in range(n):
if dist[i][k] + dist[k][j] < dist[i][j]:
dist[i][j] = dist[i][k] + dist[k][j]
# Negative cycle check: any dist[i][i] < 0 means negative cycle through i
for i in range(n):
if dist[i][i] < 0:
raise ValueError(f"Negative cycle through vertex {i}")
return dist