Skip to main content
Algorithms•Last Updated : 23 Oct 2026
~50 min read

Shortest Paths: Dijkstra and Bellman-Ford

Find the cheapest route through a weighted graph. See why BFS stops being enough, how Dijkstra's greedy expansion with a priority queue works and why it needs non-negative weights, and how Bellman-Ford copes with negative edges.

When "fewest steps" is not "cheapest"

A navigation app does not want the route with the fewest turns. It wants the one that takes the least time, or the least distance, or the least toll. Each road segment has a cost, and the best route is the one whose costs add up to the smallest total. That is the single-source shortest path problem on a weighted graph: from one starting vertex, find the cheapest path to every other vertex.

BFS cannot do it. It counts edges, so it happily prefers a direct road of 100 km over a three-hop detour of 5 km each. You need an algorithm that understands weights. The most important one, and the model for many others, is Dijkstra's algorithm.

Definition: Relaxation

The basic move of every shortest-path algorithm: if going to v through u is cheaper than the best known distance to v (dist[u] + weight(u, v) < dist[v]), update dist[v] and remember u as its predecessor.

Dijkstra: expand the closest unfinished vertex

Imagine a fire starting at your source and spreading along roads at a speed proportional to the cost. The fire reaches the nearest vertex first, then the next nearest, always in order of true distance. Dijkstra simulates exactly that.

Keep a tentative distance for every vertex: zero for the source, infinity for the rest. Repeatedly pick the unfinished vertex with the smallest tentative distance. That distance is now final. Then relax every edge leaving it, in case the new vertex offers a shorter route to a neighbour.

Why is the first pick final? Any other route to that vertex would have to leave the finished region through some other unfinished vertex whose distance is at least as large, and since weights are not negative, adding more edges can only increase the cost. This is a greedy argument, and it is exactly where negative weights break everything: a later negative edge could undercut a distance you declared final.

The efficient way to repeatedly find the smallest tentative distance is a min-heap (priority queue). Push (distance, vertex) pairs. When you pop one whose distance is larger than the best known for that vertex, it is stale; skip it. Python's heapq has no decrease-key, so pushing duplicates and skipping stale ones is the standard trick.

Relaxation on a small graph
        3         2
   A ------- B ------- C
    \_______________/
            10

start:   dist[A]=0  dist[B]=inf dist[C]=inf
expand A: dist[B]=3  dist[C]=10
expand B: 3 + 2 = 5 < 10  ->  dist[C]=5  (parent of C becomes B)
expand C: done
The direct road A-C costs 10, but A-B-C costs 3 + 2 = 5, so the tentative distance to C improves.
PythonDijkstra with heapq: distances and the actual route
1import heapq
2
3graph = {
4 "A": [("B", 7), ("C", 9), ("F", 14)],
5 "B": [("A", 7), ("C", 10), ("D", 15)],
6 "C": [("A", 9), ("B", 10), ("D", 11), ("F", 2)],
7 "D": [("B", 15), ("C", 11), ("E", 6)],
8 "E": [("D", 6), ("F", 9)],
9 "F": [("A", 14), ("C", 2), ("E", 9)],
10}
11
12def dijkstra(graph, source):
13 dist = {v: float("inf") for v in graph}
14 parent = {source: None}
15 dist[source] = 0
16 heap = [(0, source)]
17 while heap:
18 d, u = heapq.heappop(heap)
19 if d > dist[u]:
20 continue # stale entry: a better route was found already
21 for v, w in graph[u]:
22 if d + w < dist[v]: # relaxation
23 dist[v] = d + w
24 parent[v] = u
25 heapq.heappush(heap, (dist[v], v))
26 return dist, parent
27
28def route(parent, target):
29 path = []
30 while target is not None:
31 path.append(target)
32 target = parent[target]
33 return path[::-1]
34
35dist, parent = dijkstra(graph, "A")
36for v in sorted(dist):
37 print(v, dist[v], route(parent, v))
Output
A 0 ['A']
B 7 ['A', 'B']
C 9 ['A', 'C']
D 20 ['A', 'C', 'D']
E 20 ['A', 'C', 'F', 'E']
F 11 ['A', 'C', 'F']

What it costs

With a binary heap and an adjacency list, each edge triggers at most one push, and each push or pop costs O(log V) (or O(log E), which is the same up to a constant). The total is O((V + E) log V). In a sparse graph that is close to linear, which is why Dijkstra scales to road networks with millions of vertices, usually with extra tricks on top.

Two cautions. First, Dijkstra needs non-negative weights. Second, if all weights are equal, BFS does the same job in O(V + E) with a plain queue. Dijkstra is, in effect, BFS with the queue replaced by a priority queue so that "closest" means cheapest rather than fewest edges.

Choosing a shortest-path algorithm

SituationAlgorithmTime
Unweighted graphBFSO(V + E)
Non-negative weightsDijkstra with a binary heapO((V + E) log V)
Negative weights, no negative cyclesBellman-FordO(V * E)
Every pair of vertices, small graphFloyd-WarshallO(V^3)

Negative weights and Bellman-Ford

Negative edge weights are not exotic: they model a refund, an energy gain on a downhill leg, or an exchange rate expressed through logarithms. They also defeat Dijkstra, as the next example demonstrates with a tiny graph where the greedy "this distance is final" claim is simply false.

Bellman-Ford makes no such claim. It relaxes every edge, V - 1 times in total. The reason V - 1 rounds suffice: a shortest path with no repeated vertices has at most V - 1 edges, and after round k the distances are correct for all paths using up to k edges. Run one more round, and if any distance still improves, there is a negative cycle reachable from the source, and "shortest" is meaningless because you can loop forever to drive the cost down. Bellman-Ford detects that; Dijkstra silently returns nonsense.

PythonDijkstra misled by a negative edge; Bellman-Ford correct
1import heapq
2
3edges = [("S", "A", 2), ("S", "B", 3), ("B", "A", -2), ("A", "T", 1)]
4vertices = ["S", "A", "B", "T"]
5
6def bellman_ford(source):
7 dist = {v: float("inf") for v in vertices}
8 dist[source] = 0
9 for _ in range(len(vertices) - 1):
10 for u, v, w in edges:
11 if dist[u] + w < dist[v]:
12 dist[v] = dist[u] + w
13 negative_cycle = any(dist[u] + w < dist[v] for u, v, w in edges)
14 return dist, negative_cycle
15
16def dijkstra(source):
17 adj = {v: [] for v in vertices}
18 for u, v, w in edges:
19 adj[u].append((v, w))
20 dist = {v: float("inf") for v in vertices}
21 dist[source] = 0
22 done, heap = set(), [(0, source)]
23 while heap:
24 d, u = heapq.heappop(heap)
25 if u in done:
26 continue
27 done.add(u) # declared final, never revisited
28 for v, w in adj[u]:
29 if v not in done and d + w < dist[v]:
30 dist[v] = d + w
31 heapq.heappush(heap, (dist[v], v))
32 return dist
33
34print("bellman-ford:", bellman_ford("S"))
35print("dijkstra: ", dijkstra("S"))
36
37edges.append(("T", "B", -1)) # B -> A -> T -> B now sums to -2
38print("with a negative cycle:", bellman_ford("S")[1])
Output
bellman-ford: ({'S': 0, 'A': 1, 'B': 3, 'T': 2}, False)
dijkstra:     {'S': 0, 'A': 2, 'B': 3, 'T': 3}
with a negative cycle: True

Misconceptions and pitfalls

"The path with the fewest edges is the shortest." Only if all weights are equal. Otherwise two cheap edges beat one expensive one.

"Dijkstra works with negative weights if I am careful." It does not, because the finality argument fails. Use Bellman-Ford, or reweight the graph with Johnson's technique if you need many sources.

Skipping the stale-entry check. Without if d > dist[u]: continue the algorithm still returns correct distances, but it re-expands vertices with outdated distances and wastes time, possibly a great deal on dense graphs.

Storing only distances. To answer "which route?" you need the parent map. Walk it backwards from the target and reverse the result.

Using float("inf") arithmetic carelessly. Adding a weight to infinity is fine in Python; in languages with integer overflow, a "large number" sentinel plus a weight can wrap around to a negative.

Note

A* is Dijkstra with a compass: it adds a heuristic estimate of the remaining distance to the priority, so the search is pulled toward the goal. Game and map software use it when you only need one target, not all of them.

Check your understanding

These questions test the reasoning behind the algorithm.

Suggested Quiz
Question 1 of 4

Roads: A to C direct costs 10; A to B costs 3 and B to C costs 2. BFS and Dijkstra are asked for the route A to C. How do they differ?

Quick Syntax & Reference Sheet

Concept / RuleSyntax / Usage
Relaxationif dist[u] + w < dist[v]: dist[v] = dist[u] + w; parent[v] = u
Dijkstra loopd, u = heappop(heap) if d > dist[u]: continue for v, w in graph[u]: relax and heappush
Bellman-Fordrepeat V-1 times: relax every edge; one extra pass detects negative cycles
Rebuild routewalk parent[] from target to source, then reverse

Common Pitfalls & Developer Mistakes

✕ Mistake 1:

Running Dijkstra on a graph with negative edges

Why it happens: Settled distances can be undercut later, so the result may be wrong with no error.

Correction: Use Bellman-Ford, or check weights up front and reject negatives.
✕ Mistake 2:

Pushing to the heap without comparing against the current best

Why it happens: You flood the heap with useless entries and may reprocess vertices.

Correction: Push only when relaxation improves dist[v], and skip stale pops.
✕ Mistake 3:

Forgetting the parent map

Why it happens: You know the cost but not the route.

Correction: Record parent[v] = u whenever you relax an edge.

Frequently Asked Questions

Is Dijkstra a greedy algorithm?

Yes. At each step it finalises the closest unfinished vertex and never revisits it, justified by the non-negative weight argument. It is the standard worked example of greedy on graphs.

What if I need shortest paths between every pair of vertices?

For small graphs, Floyd-Warshall (O(V^3)) is simple. For large sparse graphs, run Dijkstra from each vertex, or use Johnson's algorithm when negative weights exist.
Article Tags:#Shortest Path#Dijkstra#Bellman-Ford#Priority Queue#Weighted Graphs#Relaxation
Was this lesson clear and helpful?

Discussion (0)

Sign in to ask questions and reply.
Loading discussion
Discuss in community