Dijkstra
In short: Dijkstra's algorithm finds the shortest paths from one vertex in a graph with non-negative weights by taking the closest vertex from a priority queue: O((V + E) log V).
Problem
Fewest streets is not fastest
A courier leaves the depot for a client. The app has to give the fastest route. Every street has a time in minutes: the boulevard is wide but jammed, and the small side streets are quick.
The city is a graph, and the streets are its edges in both directions (see representing a graph):
static void AddStreet(Dictionary<string, List<Street>> city, string a, string b, int minutes){ if (!city.ContainsKey(a)) city[a] = []; if (!city.ContainsKey(b)) city[b] = []; city[a].Add(new Street(b, minutes)); city[b].Add(new Street(a, minutes));}var city = new Dictionary<string, List<Street>>();AddStreet(city, "Depot", "Bridge", 2);AddStreet(city, "Depot", "Square", 5);AddStreet(city, "Depot", "Market", 15);AddStreet(city, "Bridge", "Square", 1);AddStreet(city, "Bridge", "Station", 8);AddStreet(city, "Square", "Station", 3);AddStreet(city, "Square", "Market", 9);AddStreet(city, "Station", "Client", 3);AddStreet(city, "Market", "Client", 2);The first idea is BFS from the previous lesson: it finds the shortest path. But BFS counts streets, not minutes:
static Dictionary<string, string> FewestStreets( Dictionary<string, List<Street>> city, string start){ var prev = new Dictionary<string, string>(); var seen = new HashSet<string> { start }; var queue = new Queue<string>([start]); while (queue.TryDequeue(out var place)) { foreach (Street street in city[place]) { if (seen.Add(street.To)) { prev[street.To] = place; queue.Enqueue(street.To); } } } return prev;}var city = new Dictionary<string, List<Street>>();AddStreet(city, "Depot", "Bridge", 2);AddStreet(city, "Depot", "Square", 5);AddStreet(city, "Depot", "Market", 15);AddStreet(city, "Bridge", "Square", 1);AddStreet(city, "Bridge", "Station", 8);AddStreet(city, "Square", "Station", 3);AddStreet(city, "Square", "Market", 9);AddStreet(city, "Station", "Client", 3);AddStreet(city, "Market", "Client", 2);var r = Route(FewestStreets(city, "Depot"), "Client");Console.WriteLine($"Fewest streets: {string.Join(" → ", r)} ({r.Count - 1} streets, {Minutes(city, r)} min)");Fewest streets: Depot → Market → Client (2 streets, 17 min)Two streets, but 17 minutes: 15 along the boulevard to the market and 2 to the client. Through the bridge, the square and the station it is four streets and only 9 minutes. BFS does not even look at the minutes.
The idea
The nearest place first
BFS goes by number of streets: first everything one street away, then two. Here you have to go by minutes: first the place you reach fastest, then the next one.
That is Dijkstra’s algorithm (Edsger Dijkstra, 1956). For every place it keeps the best time so far, dist, plus a queue of “minutes, place” entries.
- At the start
distis 0 for the depot, and every other place is unreached (∞). - Take the entry with the fewest minutes from the queue. That is a priority queue: it removes the smallest in O(log n).
- For every street from that place, try: does it reach the neighbour faster than before? If so, write down the new time, remember where you came from (
prev) and put a new entry in the queue. This trying is called relaxation. - Repeat until the queue is empty.
Why does the place that leaves the queue already have its final time? All other entries have at least as many minutes, and every street adds minutes. No path through them can arrive earlier.
See it
Dijkstra from the inside
static Paths Dijkstra( Dictionary<string, List<Street>> city, string start){ var dist = new Dictionary<string, int> { [start] = 0 }; var prev = new Dictionary<string, string>(); var queue = new PriorityQueue<string, int>(); queue.Enqueue(start, 0); while (queue.TryDequeue(out var place, out var minutes)) { if (minutes > dist[place]) continue; // stale foreach (Street street in city[place]) { int t = minutes + street.Minutes; if (!dist.TryGetValue(street.To, out int old) || t < old) { dist[street.To] = t; prev[street.To] = place; queue.Enqueue(street.To, t); } } } return new Paths(dist, prev);}PriorityQueue<string, int> holds the place, and the priority is the minutes. TryDequeue gives both. If TryGetValue does not find the place, it is not reached yet: that is ∞.prev keeps the route backwards: for every place, where the fastest path came from. You start at the client, follow prev to the depot and turn it round:
static List<string> Route(Dictionary<string, string> prev, string end){ var places = new List<string>(); for (string? at = end; at != null; at = prev.GetValueOrDefault(at)) { places.Add(at); } places.Reverse(); return places;}var paths = Dijkstra(city, "Depot");var r = Route(paths.Prev, "Client");Console.WriteLine($"Fastest: {string.Join(" → ", r)} ({r.Count - 1} streets, {Minutes(city, r)} min)");Fastest: Depot → Bridge → Square → Station → Client (4 streets, 9 min)static Paths Dijkstra( Dictionary<string, List<Street>> city, string start){ var dist = new Dictionary<string, int> { [start] = 0 }; var prev = new Dictionary<string, string>(); var queue = new PriorityQueue<string, int>(); queue.Enqueue(start, 0); while (queue.TryDequeue(out var place, out var minutes)) { if (minutes > dist[place]) continue; // stale foreach (Street street in city[place]) { int t = minutes + street.Minutes; if (!dist.TryGetValue(street.To, out int old) || t < old) { dist[street.To] = t; prev[street.To] = place; queue.Enqueue(street.To, t); } } } return new Paths(dist, prev);}Step by step, in words
- The courier starts at the Depot: 0 minutes. Every other place is ∞, nothing has reached it yet. The queue holds a single entry: 0 Depot.
- Take the smallest entry, 0 Depot. Each street from it gives a time: Bridge 2, Square 5, Market 15. All are better than ∞: they are written down and go into the queue.
- Next is 2 Bridge. Through it the Square is 2 + 1 = 3, better than 5: the time and prev change, and 3 Square goes in. The old 5 Square stays in the queue, it is now stale. The Station gets 10.
- 3 Square: final, nothing in the queue can make it faster. Through it the Station drops from 10 to 6 and the Market from 15 to 12.
- On top is 5 Square, but the Square already has 3. The entry is stale: skip it. That is the lazy deletion: instead of searching the queue for the old entry, you throw it away when it comes out.
- 6 Station: final. The Client gets 6 + 3 = 9. The way back, Bridge with 14 and Square with 9, is no better.
- 9 Client: final, 9 minutes. Through the Client the Market drops from 12 to 11. The frontier grows by time, not by number of streets.
- The rest: 10 Station is stale, 11 Market is final and improves nothing, 12 and 15 Market are stale. The queue is empty.
- prev gives the route backwards: Client ← Station ← Square ← Bridge ← Depot. Turned round: Depot → Bridge → Square → Station → Client, 9 minutes, four streets. The two-street route through the Market takes 17.
Places become final in order of their time: Depot 0, Bridge 2, Square 3, Station 6, Client 9, Market 11. The market, one street from the depot, is last.
Try it
Predict the output
How many minutes to the market does Dijkstra give?
var city = new Dictionary<string, List<Street>>();AddStreet(city, "Depot", "Bridge", 2);AddStreet(city, "Depot", "Square", 5);AddStreet(city, "Depot", "Market", 15);AddStreet(city, "Bridge", "Square", 1);AddStreet(city, "Bridge", "Station", 8);AddStreet(city, "Square", "Station", 3);AddStreet(city, "Square", "Market", 9);AddStreet(city, "Station", "Client", 3);AddStreet(city, "Market", "Client", 2);Console.WriteLine(Dijkstra(city, "Depot").Dist["Market"]);The idea
Stale entries
When the square gets 3 minutes instead of 5, the entry “5 Square” is already in the queue. Why not change it in place?
Built-in queues cannot: C#’s PriorityQueue has no “change priority”. Remove (since .NET 9) searches for the element linearly, O(n). So the better time goes in as a new entry. The old one stays, and when it comes out, the check throws it away: its time is greater than the one in dist. This is lazy deletion.
The queue can hold more entries than places, at most one per street. The price is a little memory and a few empty takes.
while (queue.TryDequeue(out var place, out var minutes))
{
if () continue; // staleAn entry is stale if its minutes are more than the place’s best time. Then an earlier entry has already handled the place with fewer minutes.
Try it
Find the bug
A colleague decided that the first time found is also the best, as in BFS:
static Paths Dijkstra( Dictionary<string, List<Street>> city, string start){ var dist = new Dictionary<string, int> { [start] = 0 }; var prev = new Dictionary<string, string>(); var queue = new PriorityQueue<string, int>(); queue.Enqueue(start, 0); while (queue.TryDequeue(out var place, out var minutes)) { if (minutes > dist[place]) continue; // stale foreach (Street street in city[place]) { int t = minutes + street.Minutes; if (!dist.ContainsKey(street.To)) { dist[street.To] = t; prev[street.To] = place; queue.Enqueue(street.To, t); } } } return new Paths(dist, prev);}var paths = Dijkstra(city, "Depot");var r = Route(paths.Prev, "Client");Console.WriteLine($"Fastest: {string.Join(" → ", r)} ({r.Count - 1} streets, {Minutes(city, r)} min)");Fastest: Depot → Bridge → Station → Client (3 streets, 13 min)13 minutes instead of 9. Which line has the bug?
Pick the line with the bug.
The time is written only the first time the place is reached. The square gets 5 straight from the depot, and when the bridge finds 3, it is not written. The relaxation has to compare: !dist.TryGetValue(street.To, out int old) || t < old. The first time is final in BFS because there every street costs the same.
The idea
What it costs
Every street is tried twice (once from each end) and can add an entry. Every entry goes in and comes out once, at O(log V). In total O((V + E) log V) for V places and E streets.
Without the queue, the nearest place is found with a loop over all places that are not final yet. That is how the algorithm was originally written:
static Dictionary<string, int> DijkstraScan( Dictionary<string, List<Street>> city, string start){ var dist = city.Keys.ToDictionary(place => place, _ => int.MaxValue); dist[start] = 0; var done = new HashSet<string>(); while (done.Count < city.Count) { string? best = null; foreach (string place in city.Keys) { if (done.Contains(place)) continue; if (best == null || dist[place] < dist[best]) best = place; } if (dist[best!] == int.MaxValue) break; done.Add(best!); foreach (Street street in city[best!]) { int t = dist[best!] + street.Minutes; if (t < dist[street.To]) dist[street.To] = t; } } return dist;}var city = new Dictionary<string, List<Street>>();AddStreet(city, "Depot", "Bridge", 2);AddStreet(city, "Depot", "Square", 5);AddStreet(city, "Depot", "Market", 15);AddStreet(city, "Bridge", "Square", 1);AddStreet(city, "Bridge", "Station", 8);AddStreet(city, "Square", "Station", 3);AddStreet(city, "Square", "Market", 9);AddStreet(city, "Station", "Client", 3);AddStreet(city, "Market", "Client", 2);var dist = DijkstraScan(city, "Depot");Console.WriteLine(string.Join(", ", new[] { "Depot", "Bridge", "Square", "Station", "Market", "Client" } .Select(p => $"{p} {dist[p]}")));Depot 0, Bridge 2, Square 3, Station 6, Market 11, Client 9The answer is the same. But the loop is V steps, and it runs V times: O(V²).
Run both versions on a district where the crossings form a grid, each one linked to its right and lower neighbour. Every street takes 1 to 9 minutes. Every comparison of two times is counted: when looking for the nearest place, inside the queue and when trying a street.
- No queue: a loop for the nearest
O(n²) - With a priority queue
O(n log n)
| Algorithm | n = 100 | n = 1,000 | n = 10,000 | n = 100,000 |
|---|---|---|---|---|
| No queue: a loop for the nearest | 5,410 | 504,372 | ≈ 50,437,200 | ≈ 5,043,720,000 |
| With a priority queue | 1,269 | 17,121 | 213,675 | 2,554,618 |
static Paths Dijkstra( Dictionary<string, List<Street>> city, string start){ var dist = new Dictionary<string, int> { [start] = 0 }; var prev = new Dictionary<string, string>(); var queue = new PriorityQueue<string, int>(); queue.Enqueue(start, 0); while (queue.TryDequeue(out var place, out var minutes)) { if (minutes > dist[place]) continue; // stale foreach (Street street in city[place]) { int t = minutes + street.Minutes; if (!dist.TryGetValue(street.To, out int old) || t < old) { dist[street.To] = t; prev[street.To] = place; queue.Enqueue(street.To, t); } } } return new Paths(dist, prev);}At 1000 crossings the version without a queue makes 504 372 comparisons, the one with a queue 17 121. At 100 000 crossings, the size of a big city, the queue makes 2 554 618. Without it there would be about 5 billion: Bench does not run them but estimates them from the curve.
More precisely, if you are curious
When is O(V²) better? When the graph is dense: every place is linked to almost every other, and E is about V². Then the queue does O(V² log V) and the loop O(V²). In a road network every crossing has 3–4 streets, the graph is sparse and the queue wins.
Negative weights break Dijkstra. If a street costs −5 (say, a delivery bonus on the way), a place that is already final can become nearer through it later, and the algorithm no longer looks at it. Such graphs need Bellman-Ford: it tries every street V − 1 times, O(V · E), and reports a cycle that lowers the time forever.
A trap in Java and C#: ∞ is often written as int.MaxValue. Add minutes to it and the number overflows into a negative: the unreached place looks like the nearest. That is why dijkstraScan stops as soon as the nearest is ∞, and dijkstra does not store ∞ at all: a missing place is unreached.
The navigation on your phone does not run Dijkstra over a whole continent. It uses A*: Dijkstra that adds to the minutes in the queue an estimate of how much is left in a straight line, so it heads for the goal instead of spreading in every direction. OSPF, the protocol routers in one network use to find the path for packets, runs real Dijkstra.
Exercise
A closed street
Solve it in your editor.
Roadworks. The street between the bridge and the square is closed. Remove it from the city (in both directions) and print the fastest route and minutes to the station, the client and the market:
Station: Depot → Square → Station (8 min)Client: Depot → Square → Station → Client (11 min)Market: Depot → Square → Station → Client → Market (13 min)The market is still fastest through the client: 13 minutes against 14 through the square and 15 along the boulevard.
More tasks
- Stop in time. If you only care about the client, stop as soon as it leaves the queue. How many entries are left untaken?
- Count the takes. Count how many entries left the queue and how many of them were stale.
- Two couriers. Two depots, one client: which courier gets there first? Hint: put both depots in the queue with 0 minutes.
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Priority queue · Graph: adjacency list and matrix · BFS: the fewest stops · Topological sorting
- Continues in: Minimum spanning tree · City navigation
- Tasks on this topic: Graphs