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*.
The greedy idea
Give every vertex a tentative distance: 0 for the source, ∞ for the rest. Pick the unfinished vertex with the smallest tentative distance. Its distance is now final. For each of its neighbours, check whether going through it is shorter (“relax the edge”): if dist[u] + w < dist[v], updatedist[v]and remember u as v’s predecessor.Repeat until every reachable vertex is final.
Dijkstra with PriorityQueue
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”).
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);}A:0 B:4 C:9 D:2 E:7 F:10 G:9 A → D → G → F
prev map is a shortest-path tree: the best route from the source to every place at once.
Negative weights break it
A*: Dijkstra with a sense of direction
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.
| Situation | Algorithm |
|---|---|
{ {1,3,1,2}, {1,5,1,9}, {4,2,1,1} }. Expected: 7.
Show a hint
PriorityQueue<(int R, int C), int>.
Show the solution
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;}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.