<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. DSA
  3. Graphs
All lessons in this course

Lesson 35 of 50 15 minAdvanced

Shortest paths: Dijkstra

Find the quickest routes from one place to all others in a weighted graph: Dijkstra’s greedy idea, a PriorityQueue implementation, rebuilding the route, why negative weights break it, and A*.

BFS counts steps. Real routes have weights: a road takes 2 minutes or 7 minutes, a cable run costs per metre. The route with the fewest roads is not always the quickest. Dijkstra’s algorithm (1956) finds the shortest weighted path from one source to every other vertex, as long as no weight is negative.

The greedy idea

  1. Give every vertex a tentative distance: 0 for the source, ∞ for the rest.
  2. Pick the unfinished vertex with the smallest tentative distance. Its distance is now final.
  3. For each of its neighbours, check whether going through it is shorter (“relax the edge”): if dist[u] + w < dist[v], update dist[v] and remember u as v’s predecessor.
  4. Repeat until every reachable vertex is final.

Why is the smallest one safe to finalise? Any other route to it would have to leave the finished area through some vertex that is already at least as far away, and then add non-negative weights. It can only be longer or equal.

Interactive

Dijkstra’s shortest paths

Driving times in minutes. Find the quickest route from the gate A to every place. Then pick a destination to see its route.

42365234716A0B∞C∞D∞E∞F∞G∞
PlaceABCDEFG
Minutes0∞∞∞∞∞∞
Via–––––––
Route to
Start: the gate A is 0 minutes away; everything else is ∞ (not reached yet).

Dijkstra with PriorityQueue

Finding the smallest tentative distance is a job for a min-heap. .NET’s PriorityQueue has no “decrease priority” operation, so the standard trick is to add the vertex again with the new distance, and skip outdated entries when they come out (“lazy deletion”).

Dijkstra.cs
var graph = new Dictionary<char, List<(char To, int W)>>();void Road(char a, char b, int w){    if (!graph.ContainsKey(a)) graph[a] = new();    if (!graph.ContainsKey(b)) graph[b] = new();    graph[a].Add((b, w));    graph[b].Add((a, w));}Road('A', 'B', 4); Road('A', 'D', 2); Road('B', 'E', 3); Road('B', 'C', 6);Road('D', 'E', 5); Road('C', 'E', 2); Road('C', 'F', 3); Road('E', 'G', 4);Road('D', 'G', 7); Road('F', 'G', 1); Road('E', 'F', 6); var (dist, prev) = Dijkstra(graph, 'A');foreach (var place in dist.Keys.Order()) Console.Write($"{place}:{dist[place]} ");Console.WriteLine(); var route = new List<char> { 'F' };while (prev.TryGetValue(route[^1], out char p)) route.Add(p);route.Reverse();Console.WriteLine(string.Join(" → ", route)); static (Dictionary<char, int>, Dictionary<char, char>) Dijkstra(Dictionary<char, List<(char To, int W)>> g, char source){    var best = new Dictionary<char, int> { [source] = 0 };    var from = new Dictionary<char, char>();    var pq = new PriorityQueue<char, int>();    pq.Enqueue(source, 0);    while (pq.TryDequeue(out char u, out int d))    {        if (d > best[u]) continue;                     // outdated entry: u was improved later        foreach (var (v, w) in g[u])        {            int nd = d + w;            if (nd < best.GetValueOrDefault(v, int.MaxValue))            {                best[v] = nd;                from[v] = u;                pq.Enqueue(v, nd);                     // no decrease-key: just add it again            }        }    }    return (best, from);}
Output
A:0 B:4 C:9 D:2 E:7 F:10 G:9 
A → D → G → F

With a binary heap the cost is O((V + E) log V). For a road network with a million junctions that is a fraction of a second. The prev map is a shortest-path tree: the best route from the source to every place at once.

Negative weights break it

Dijkstra finalises a vertex as soon as it is the closest. If some edge has a negative weight, a longer-looking route could become shorter later, after the vertex was already finalised. Example: S → A costs 2, S → B costs 5, B → A costs −4. Dijkstra finalises A at 2, but S → B → A costs 1. For negative weights, use Bellman-Ford (next lesson).

A*: Dijkstra with a sense of direction

Dijkstra spreads in all directions equally. When you only need the route to one target and can estimate the remaining distance (for example the straight-line distance on a map), A\* orders the queue by distance so far + estimate. It heads towards the target and explores far fewer vertices. As long as the estimate never overestimates, the route is still optimal. Navigation apps and game path-finding use A* and its variants.

SituationAlgorithm
All edges cost the sameBFS: O(V + E)
Non-negative weights, one sourceDijkstra: O((V + E) log V)
Non-negative weights, one target, good estimateA*
Negative weightsBellman-Ford: O(V · E)
All pairs, small graphFloyd-Warshall: O(V³)
Quick check

Dijkstra has just finalised vertex X at distance 12. Can a shorter route to X be found later, if all weights are ≥ 0?

Quick check

Why does the code skip entries with d > best[u]?

Practice

A site is a grid of ground costs (effort to drive over each cell). Moving up, down, left or right into a cell costs that cell’s value; the start cell is free. Find the cheapest total cost from the top-left to the bottom-right of { {1,3,1,2}, {1,5,1,9}, {4,2,1,1} }. Expected: 7.

Show a hint

Each cell is a vertex; its neighbours are the 4 cells around it, and the edge weight is the cost of the cell you move into. Run Dijkstra with a PriorityQueue<(int R, int C), int>.

Show the solution
Solution.cs
int[,] cost = { { 1, 3, 1, 2 }, { 1, 5, 1, 9 }, { 4, 2, 1, 1 } };Console.WriteLine(CheapestCrossing(cost)); static int CheapestCrossing(int[,] cost){    int rows = cost.GetLength(0), cols = cost.GetLength(1);    var best = new int[rows, cols];    for (int r = 0; r < rows; r++)        for (int c = 0; c < cols; c++) best[r, c] = int.MaxValue;    best[0, 0] = 0;     var pq = new PriorityQueue<(int R, int C), int>();    pq.Enqueue((0, 0), 0);    (int Dr, int Dc)[] moves = { (-1, 0), (1, 0), (0, -1), (0, 1) };    while (pq.TryDequeue(out var cell, out int d))    {        if (d > best[cell.R, cell.C]) continue;        if (cell == (rows - 1, cols - 1)) return d;        foreach (var (dr, dc) in moves)        {            int nr = cell.R + dr, nc = cell.C + dc;            if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue;            int nd = d + cost[nr, nc];            if (nd < best[nr, nc]) { best[nr, nc] = nd; pq.Enqueue((nr, nc), nd); }        }    }    return -1;}
Output
7

Remember

  • Dijkstra repeatedly finalises the closest unfinished vertex and relaxes its edges.
  • With a PriorityQueue and lazy deletion it runs in O((V + E) log V).
  • Store predecessors to rebuild routes; together they form a shortest-path tree.
  • It requires non-negative weights. A* adds a distance estimate to reach one target faster.
0 of 50 lessons completed