Master all shortest path algorithms — BFS for unweighted graphs, Dijkstra for non-negative weights, Bellman-Ford for negative weights, Floyd-Warshall for all-pairs, DAG shortest paths, A* heuristic search, and a decision guide for choosing the right algorithm for every scenario.
Overview
Finding shortest paths in graphs is one of the most fundamental problems in computer science. The right algorithm depends on:
- Is the graph weighted or unweighted?
- Are there negative edge weights?
- Do you need one source or all pairs?
- Is the graph a DAG?
Algorithm 2 — Dijkstra (Non-Negative Weights)
import heapq
def dijkstra(graph, source):
"""
Dijkstra for non-negative weighted graphs.
Time: O((V+E) log V), Space: O(V)
"""
dist = {v: float('inf') for v in graph}
dist[source] = 0
pq = [(0, source)]
while pq:
d, u = heapq.heappop(pq)
if d > dist[u]: continue
for v, w in graph[u]:
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
heapq.heappush(pq, (dist[v], v))
return dist
# Correct for non-negative weights
graph = {0: [(1,1),(2,4)], 1: [(2,2),(3,5)], 2: [(3,1)], 3: []}
print(dijkstra(graph, 0)) # {0:0, 1:1, 2:3, 3:4}
Use when: Non-negative edge weights, single source.
Algorithm 3 — Bellman-Ford (Negative Weights)
def bellman_ford(edges, n, source):
"""
Bellman-Ford for graphs with negative weights.
Time: O(V × E), Space: O(V)
Detects negative cycles.
"""
dist = [float('inf')] * n
dist[source] = 0
for _ in range(n - 1):
for u, v, w in edges:
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
# Negative cycle check
for u, v, w in edges:
if dist[u] + w < dist[v]:
return None # Negative cycle
return dist
# Handles negative edges (Dijkstra would fail here)
edges = [(0,1,4),(0,2,2),(2,1,-3),(1,3,1)]
print(bellman_ford(edges, 4, 0)) # [0, -1, 2, 0]
Use when: Negative edge weights, single source, need negative cycle detection.
Algorithm 4 — Floyd-Warshall (All Pairs)
def floyd_warshall(n, adj):
"""
All-pairs shortest paths.
Time: O(V³), Space: O(V²)
adj[i][j] = weight (inf if no edge)
"""
dist = [row[:] for row in adj]
for k in range(n):
for i in range(n):
for j in range(n):
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
return dist
INF = float('inf')
adj = [[0,3,INF,7],[8,0,2,INF],[5,INF,0,1],[2,INF,INF,0]]
result = floyd_warshall(4, adj)
for row in result:
print([x if x < INF else '∞' for x in row])
Use when: Need shortest path between ALL pairs of vertices.
Algorithm 5 — DAG Shortest Path (Topological + DP)
When the graph is a DAG, we can find shortest paths in O(V+E) — faster than Dijkstra:
from collections import defaultdict, deque
def dag_shortest_path(graph, n, source):
"""
Shortest path in DAG using topological order.
Time: O(V + E), Space: O(V)
Works with negative weights! (no negative cycles in DAG)
"""
# Topological sort (Kahn's)
in_degree = defaultdict(int)
for u in range(n):
for v, w in graph[u]:
in_degree[v] += 1
queue = deque(i for i in range(n) if in_degree[i] == 0)
topo = []
while queue:
u = queue.popleft(); topo.append(u)
for v, w in graph[u]:
in_degree[v] -= 1
if in_degree[v] == 0: queue.append(v)
# Relax edges in topological order
dist = [float('inf')] * n
dist[source] = 0
for u in topo:
for v, w in graph[u]:
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
return dist
# DAG with negative edges — Dijkstra would fail, Bellman-Ford would be O(VE)
graph = {0: [(1,2),(2,6)], 1: [(3,7),(4,3)], 2: [(4,-4)], 3: [(5,1)], 4: [(5,5)], 5: []}
print(dag_shortest_path(graph, 6, 0)) # [0, 2, 6, 9, 2, 7]
Use when: Graph is a DAG — handles negative weights in O(V+E).
Algorithm 6 — A* Algorithm (Heuristic Search)
A* is Dijkstra enhanced with a heuristic that estimates remaining distance to the target:
import heapq
def a_star(graph, source, target, heuristic):
"""
A* search: Dijkstra + heuristic guidance.
f(n) = g(n) + h(n)
g(n) = actual cost from source to n
h(n) = estimated cost from n to target (must be admissible: never overestimate)
Time: O(E log V) but often much faster due to heuristic pruning.
"""
g = {source: 0}
f = {source: heuristic(source, target)}
pq = [(f[source], source)]
parent = {source: None}
while pq:
_, u = heapq.heappop(pq)
if u == target:
path = []
while u is not None:
path.append(u); u = parent[u]
return g[target], list(reversed(path))
for v, w in graph[u]:
new_g = g[u] + w
if v not in g or new_g < g[v]:
g[v] = new_g
f[v] = new_g + heuristic(v, target)
parent[v] = u
heapq.heappush(pq, (f[v], v))
return float('inf'), [] # Target not reachable
# Example: Grid with Manhattan distance heuristic
def manhattan(a, b):
return abs(a[0]-b[0]) + abs(a[1]-b[1])
Use when: Single target, admissible heuristic available (GPS navigation, game AI).
Algorithm Selection Guide
| YES | BFS O(V+E) — simple, optimal |
| YES | DAG Shortest Path O(V+E) — handles negative weights too! |
| YES | Bellman-Ford O(VE) — detects and reports negative cycles |
| NO | Bellman-Ford O(VE) — still correct, just slower |
| YES | Dijkstra O((V+E) log V) — fastest for non-negative weights |
| Small graph (V ≤ 1000)? | Floyd-Warshall O(V³) |
| Large sparse graph? | Dijkstra × V times O(V(V+E) log V) |
| YES | A* — often much faster than Dijkstra in practice |
Comparison Table
| Algorithm | Type | Time | Space | Neg Weights | Neg Cycle | All Pairs |
|---|
| BFS | Unweighted | O(V+E) | O(V) | ❌ | ❌ | ❌ |
| Dijkstra | Non-neg weighted | O((V+E)logV) | O(V) | ❌ | ❌ | ❌ |
| Bellman-Ford | Any weighted | O(VE) | O(V) | ✅ | ✅ | ❌ |
| DAG SP | DAG (any weight) | O(V+E) | O(V) | ✅ | N/A | ❌ |
| Floyd-Warshall | Any weighted | O(V³) | O(V²) | ✅ | ✅ | ✅ |
| A* | Non-neg + heuristic | O(E log V) | O(V) | ❌ | ❌ | ❌ |
Summary
Choosing the right shortest path algorithm is critical for efficiency:
| Scenario | Algorithm |
|---|
| Unweighted graph | BFS |
| Non-negative, single source | Dijkstra |
| Negative weights, single source | Bellman-Ford |
| DAG with any weights | Topological + DP |
| All pairs | Floyd-Warshall |
| Single target + heuristic | A* |
*Next Lesson: Union-Find DSU*