Of all the shortest path algorithms I’ve studied, Dijkstra’s algorithm is the one I reach for most often, simply because so many real-world graphs — road networks, network routing tables, dependency graphs with non-negative costs — fit its core assumption of non-negative edge weights. It finds the shortest path from a single source vertex to every other vertex in a weighted graph, and it does so by always greedily expanding the closest unvisited vertex first. I find its greedy strategy satisfying precisely because it’s provably correct under the right conditions, and understanding why it works, and where it breaks down (negative weights), has taught me a lot about the boundaries of greedy algorithms in general.
History and Background
I credit this algorithm to Edsger W. Dijkstra, a Dutch computer scientist who conceived of it in 1956 and published it in 1959 in a short paper titled “A note on two problems in connexion with graphs.” What I find charming about the origin story, which Dijkstra himself recounted later in life, is that he designed the algorithm in about twenty minutes while sitting in a café in Amsterdam with his fiancée, thinking about how to demonstrate the capabilities of a new computer, the ARMAC, by solving a practical problem: finding the shortest route between two cities in the Netherlands. He didn’t have paper at the time, so he worked it out mentally, and it later became one of the most widely taught and used algorithms in computer science.
Problem Statement
I want to find the shortest path from a single source vertex to every other vertex in a weighted graph where all edge weights are non-negative. If negative weights are present, Dijkstra’s greedy strategy can produce incorrect results, since it assumes that once a vertex’s shortest distance has been finalized, no future relaxation could ever improve it — an assumption that breaks down when negative edges allow a “cheaper” path to be discovered later. Given this restriction, Dijkstra’s algorithm solves the single-source shortest path problem efficiently, typically much faster than the more general Bellman-Ford algorithm when the non-negative weight condition holds.
Core Concepts
- Priority queue (min-heap): the data structure I use to always efficiently select the unvisited vertex with the smallest known tentative distance.
- Tentative distance: my current best estimate of the shortest distance from the source to a vertex, which can be improved (decreased) as the algorithm progresses, until it’s finalized.
- Visited/finalized set: the set of vertices whose shortest distance has been conclusively determined and will never change again.
- Relaxation: the same operation as in Bellman-Ford — checking if going through vertex
uoffers a shorter path tovthan currently known, and updating if so. - Greedy choice property: the assumption underlying Dijkstra’s correctness — that the unvisited vertex with the smallest tentative distance has, in fact, already reached its true shortest distance and can be safely finalized.
How It Works
I proceed as follows:
- I initialize the distance to the source as
0, and to every other vertex as infinity, and I place all vertices into a priority queue keyed by their tentative distance. - I repeatedly extract the vertex
uwith the smallest tentative distance from the priority queue. - I mark
uas finalized (visited) — its distance is now guaranteed correct. - For every neighbor
vofuconnected by an edge of weightw, I check ifdist[u] + w ; if so, I updatedist[v]and adjust its position in the priority queue. - I repeat steps 2-4 until the priority queue is empty (or until I’ve finalized the specific destination I care about, if I only need a single target).
Working Principle
The mechanism rests on a greedy assumption that turns out to be provably valid whenever all edge weights are non-negative: the unvisited vertex with the smallest current tentative distance cannot possibly be improved later. This is because any alternative path to that vertex would have to go through some other unvisited vertex, but since all unvisited vertices currently have tentative distances greater than or equal to the one I’m about to finalize, and since edge weights are non-negative, any such alternative path could only be equal to or longer than the direct path already found. This is precisely the property that fails when negative weights are allowed — a currently “longer” path through an unvisited vertex could, with a negative edge, end up shorter than what was just finalized, which is why Dijkstra’s algorithm cannot handle negative weights correctly.
Mathematical Foundation
I formalize the greedy choice property as follows. Let S be the set of finalized vertices at some point during execution, and let u be the unvisited vertex with the minimum tentative distance dist[u]. I claim dist[u] is the true shortest distance from the source s to u. Suppose, for contradiction, that some shorter path P from s to u exists. Since s \in S and u \notin S, path P must cross from S to the outside of S at some edge (x, y), where x \in S and y \notin S. Because edge weights are non-negative:
$$ dist[y] \leq \text{length of the portion of } P \text{ from } s \text{ to } y \leq \text{length of } P
But this contradicts the choice of u as the vertex with minimum tentative distance among unvisited vertices, since y would then have a smaller tentative distance than u. Therefore no shorter path P can exist, proving dist[u] is already correct.
Using a binary heap-based priority queue, the total time complexity is:
$$ T(|V|, |E|) = O\big((|V| + |E|) \log |V|\big) $$
which, with a Fibonacci heap, can be improved to O(|E| + |V| \log |V|).
Diagrams
flowchart TD
A["Initialize dist[source]=0, all others=infinity; add all to priority queue"] --> B{Priority queue empty?}
B -- Yes --> H[All shortest distances finalized]
B -- No --> C["Extract vertex u with minimum tentative distance"]
C --> D[Mark u as finalized]
D --> E["For each neighbor v of u with edge weight w"]
E --> F{"dist[u] + w G["dist[v] = dist[u] + w; update priority queue"]
F -- No --> B
G --> B
Pseudocode
function dijkstra(graph, source, V):
dist = array of size V, initialized to infinity
dist[source] = 0
visited = set of size V, all false
priorityQueue = min-heap keyed by dist, initially containing (0, source)
while priorityQueue is not empty:
(d, u) = priorityQueue.extractMin()
if visited[u]:
continue
visited[u] = true
for each neighbor v of u with edge weight w:
if not visited[v] and dist[u] + w
Step-by-Step Example
Consider a graph with vertices S, A, B, C and edges: S->A (2), S->B (6), A->B (3), A->C (8), B->C (1), source S.
Initialization: dist = {S: 0, A: ∞, B: ∞, C: ∞}, priority queue = [(0, S)].
Step 1: Extract S (distance 0). Relax neighbors: dist[A] = min(∞, 0+2) = 2; dist[B] = min(∞, 0+6) = 6. Queue now has [(2, A), (6, B)].
Step 2: Extract A (distance 2, the smallest). Finalize A. Relax neighbors: dist[B] = min(6, 2+3) = 5; dist[C] = min(∞, 2+8) = 10. Queue now has [(5, B), (6, B) stale, (10, C)].
Step 3: Extract B (distance 5, the smallest valid entry). Finalize B. Relax neighbors: dist[C] = min(10, 5+1) = 6. Queue now has [(6, B) stale, (6, C), (10, C) stale].
Step 4: Extract the stale (6, B) entry, skip since B is already visited. Extract (6, C). Finalize C.
Final distances: S: 0, A: 2, B: 5, C: 6
Time Complexity
- Best case:
O((|V| + |E|) \log |V|)using a binary heap — the algorithm’s structure doesn’t allow for a meaningfully better best case, since every vertex and edge must still be considered. - Average case:
O((|V| + |E|) \log |V|). - Worst case:
O((|V| + |E|) \log |V|)with a binary heap, orO(|E| + |V| \log |V|)with a Fibonacci heap. A naive array-based implementation (without a heap) instead givesO(|V|^2), which can actually be preferable on very dense graphs.
Space Complexity
Dijkstra’s algorithm requires O(|V|) space for the distance array and the visited/finalized set, plus O(|V|) space for the priority queue (which may temporarily hold up to O(|E|) entries if stale entries aren’t removed eagerly, though this doesn’t change the asymptotic space complexity given typical heap implementations). The graph itself, using an adjacency list, requires O(|V| + |E|) space.
Correctness Analysis
The correctness proof follows directly from the greedy choice property I derived in the Mathematical Foundation section: at every step, the unvisited vertex with the smallest tentative distance is guaranteed to have already reached its true shortest distance, given non-negative edge weights. I prove this by strong induction on the order in which vertices are finalized: the base case is the source itself, finalized at distance 0, which is trivially correct. For the inductive step, assuming every previously finalized vertex has a correct shortest distance, the contradiction argument shown earlier proves that the next vertex extracted from the priority queue must also have a correct shortest distance, since any alternative shorter path would necessarily pass through an unvisited vertex with an even smaller (contradictory) tentative distance. By induction, every vertex is correctly finalized in the order it is extracted from the priority queue.
Advantages
- Efficient for graphs with non-negative weights, especially with a good priority queue implementation.
- Simple greedy logic that is intuitive once the non-negative weight assumption is understood.
- Can be stopped early if only the shortest path to a specific target vertex (not all vertices) is needed, saving unnecessary computation.
- Forms the basis for many practical routing and navigation systems due to its efficiency and reliability under realistic (non-negative) cost assumptions.
Disadvantages
- Fails to produce correct results in the presence of negative edge weights, unlike Bellman-Ford.
- Requires an efficient priority queue implementation to achieve its best theoretical performance; a naive implementation degrades to
O(|V|^2). - Doesn’t directly detect negative cycles, since it isn’t designed to handle negative weights at all.
- Slightly more complex to implement correctly than Bellman-Ford, due to the need for a working priority queue and careful handling of stale/duplicate entries.
Applications
- GPS navigation systems and mapping services (like Google Maps) for computing shortest or fastest routes between locations.
- Network routing protocols such as OSPF (Open Shortest Path First), which is directly based on Dijkstra’s algorithm.
- Robotics and AI pathfinding, particularly as a building block or comparison baseline for more specialized algorithms like A*.
- Telecommunications network design, for minimizing latency or cost between network nodes.
- Flight and transportation itinerary planning, computing minimum-cost or minimum-time routes across a network of connections.
Implementation in C
#include stdio.h>
#include limits.h>
#include stdbool.h>
#define V 4
int minDistance(int dist[], bool visited[]) {
int min = INT_MAX, minIndex = -1;
for (int v = 0; v V; v++) {
if (!visited[v] && dist[v] min) {
min = dist[v];
minIndex = v;
}
}
return minIndex;
}
void dijkstra(int graph[V][V], int source) {
int dist[V];
bool visited[V];
for (int i = 0; i V; i++) {
dist[i] = INT_MAX;
visited[i] = false;
}
dist[source] = 0;
for (int count = 0; count V - 1; count++) {
int u = minDistance(dist, visited);
if (u == -1) break; /* remaining vertices unreachable */
visited[u] = true;
for (int v = 0; v V; v++) {
if (!visited[v] && graph[u][v] != 0 &&
dist[u] != INT_MAX &&
dist[u] + graph[u][v] dist[v]) {
dist[v] = dist[u] + graph[u][v];
}
}
}
printf("Vertex\tDistance from Source\n");
for (int i = 0; i V; i++) {
printf("%d\t%d\n", i, dist[i]);
}
}
int main() {
/* 0=S, 1=A, 2=B, 3=C. 0 means no direct edge. */
int graph[V][V] = {
{0, 2, 6, 0},
{2, 0, 3, 8},
{6, 3, 0, 1},
{0, 8, 1, 0}
};
dijkstra(graph, 0);
return 0;
}
Sample Input and Output
Input: graph with vertices S(0), A(1), B(2), C(3) and edges S-A(2), S-B(6), A-B(3), A-C(8), B-C(1), source S.
Output:
Vertex Distance from Source
0 0
1 2
2 5
3 6
Optimization Techniques
- Binary heap or Fibonacci heap: replacing a naive linear scan for the minimum-distance vertex with a proper heap-based priority queue drastically improves performance on sparse graphs.
- Lazy deletion: rather than trying to update a vertex’s priority in place within the heap (which many heap implementations don’t support efficiently), I simply insert a new entry each time a distance improves, and skip any “stale” entries when they’re later extracted — this trades a bit of extra memory for simpler, faster code.
- Bidirectional search: when I only need the shortest path between two specific vertices (not all pairs from the source), running Dijkstra simultaneously from both the source and destination and stopping when the searches meet can substantially reduce the number of vertices explored.
- A* search: adding a consistent heuristic function that estimates remaining distance to the target can guide the search more directly, often outperforming plain Dijkstra’s algorithm in practice for point-to-point queries, such as in maps and games.
Common Mistakes
- Applying Dijkstra’s algorithm to graphs with negative edge weights, silently producing incorrect results without any warning, since the algorithm has no built-in way to detect this violation of its core assumption.
- Forgetting to skip already-visited/finalized vertices when extracting from the priority queue, which can lead to redundant or incorrect relaxation if stale entries aren’t properly ignored.
- Using an inefficient priority queue (or none at all) on large sparse graphs, resulting in unnecessarily poor performance compared to what a proper heap-based implementation would achieve.
- Confusing Dijkstra’s single-source shortest path with the all-pairs shortest path problem, and unnecessarily running it once per vertex when Floyd-Warshall (for dense graphs) might be simpler, or when Johnson’s algorithm (for sparse graphs) would be more efficient.
Further Reading
- Dijkstra, E. W. “A note on two problems in connexion with graphs,” Numerische Mathematik, 1959: https://link.springer.com/article/10.1007/BF01386390
- Cormen, T. H., Leiserson, C. E., Rivest, R. L., Stein, C. “Introduction to Algorithms” (CLRS), Chapter on Single-Source Shortest Paths: https://mitpress.mit.edu/9780262046305/introduction-to-algorithms/
- GeeksforGeeks, “Dijkstra’s Shortest Path Algorithm”: https://www.geeksforgeeks.org/dsa/dijkstras-shortest-path-algorithm-greedy-algo-7/
- Wikipedia, “Dijkstra’s algorithm”: https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm
