{"id":"dsa-graphs-dijkstra","slug":"dijkstra","title":"Dijkstra's shortest paths","text":"Until now, every edge cost one step. Real routes have **weights**: minutes of driving, the price of a flight, the delay of a network link. Then the fewest edges aren't always the cheapest: one road of 10 minutes from A to B loses to three roads of 1 minute each, from A through C and D to B, though BFS would pick the single road. **Dijkstra's algorithm** finds the cheapest way from one start to every vertex, as long as no weight is negative. A weighted adjacency list keeps a pair for each edge, the neighbor and the weight.\n\n### Like BFS, with a heap\n\nBFS takes the vertices in order of their number of steps. Dijkstra takes them in order of their **distance**, the total weight of the best way found so far, by keeping them in a heap of `(distance, vertex)` pairs: `heapq` pops the smallest pair, and pairs compare by their first item first.\n\n1. The start has distance 0, and the heap holds `(0, start)`.\n2. Pop the closest pair, `(d, v)`. If v already has a smaller distance, the pair is stale (more on that below): skip it. Otherwise `d` is v's final distance.\n3. For each edge from v to w with weight `cost`, the way through v reaches w for `d + cost`. If w has no distance yet, or a bigger one, store the new one and push `(d + cost, w)`. This is called **relaxing** the edge.\n\n```python\nimport heapq\n\n\ndef dijkstra(graph, start):\n    dist = {start: 0}\n    heap = [(0, start)]\n    while heap:\n        d, v = heapq.heappop(heap)\n        if d > dist[v]:\n            print(\"skip\", v, d)\n            continue\n        print(\"take\", v, d)\n        for w, cost in graph[v]:\n            if w not in dist or d + cost < dist[w]:\n                dist[w] = d + cost\n                heapq.heappush(heap, (d + cost, w))\n    return dist\n\n\nroads = {\n    \"A\": [(\"B\", 10), (\"C\", 1)],\n    \"B\": [(\"A\", 10), (\"D\", 1), (\"E\", 2)],\n    \"C\": [(\"A\", 1), (\"D\", 1)],\n    \"D\": [(\"C\", 1), (\"B\", 1), (\"E\", 7)],\n    \"E\": [(\"B\", 2), (\"D\", 7)],\n}\nprint(dijkstra(roads, \"A\"))\n```\n\n```output\ntake A 0\ntake C 1\ntake D 2\ntake B 3\ntake E 5\nskip E 9\nskip B 10\n{'A': 0, 'B': 3, 'C': 1, 'D': 2, 'E': 5}\n```\n\n### Skipping stale entries\n\n`heapq` can't lower the distance of a pair that's already in the heap. So when a cheaper way to a vertex turns up, Dijkstra pushes a new pair and leaves the old one behind. B was pushed at 10 (straight from A) and later at 3 (through C and D), and E at 9 and then at 5. When an old pair finally comes out, its distance is bigger than `dist[v]`: it's **stale**, and `continue` skips it. Without that check the answers stay right, but every stale pair relaxes all the edges of its vertex again, for nothing.\n\n### Why it's right, and why no weight may be negative\n\nWhen `(d, v)` is the smallest pair in the heap, could a cheaper way to v still be found later? That way would have to leave the vertices already taken through some vertex x that is still waiting in the heap, at a distance of at least d, and the edges after x only add to that. So it costs at least d, and d is final. That's also why a search for one goal can stop as soon as the goal comes out of the heap.\n\nThe argument needs \"only add\". A negative weight can make a later way cheaper, and then a distance taken as final isn't:\n\n```python\nimport heapq\n\n\ndef cheapest(graph, start, goal):\n    dist = {start: 0}\n    heap = [(0, start)]\n    while heap:\n        d, v = heapq.heappop(heap)\n        if v == goal:\n            return d\n        if d > dist[v]:\n            continue\n        for w, cost in graph[v]:\n            if w not in dist or d + cost < dist[w]:\n                dist[w] = d + cost\n                heapq.heappush(heap, (d + cost, w))\n    return None\n\n\ngraph = {\"A\": [(\"B\", 2), (\"C\", 5)], \"B\": [], \"C\": [(\"B\", -4)]}\nprint(cheapest(graph, \"A\", \"B\"))\n```\n\n```output\n2\n```\n\nB came out first, at 2, and was taken as final. But A, C, B costs 5 - 4 = 1. Graphs with negative weights need a slower algorithm, such as Bellman-Ford, O(V·E).\n\n### What it costs\n\nEach edge pushes at most one pair, so the heap never holds more than E + 1 pairs, and each push and pop is O(log E), which is O(log V): E is at most V², so log E is at most 2 log V. In all, O((V + E) log V) time and O(V + E) memory.\n\nThe tempting way, trying every path and keeping the cheapest, explodes. In a **ladder**, two rows of vertices joined by rungs, each rung doubles the paths from one corner to the opposite one:\n\n```python\ndef count_paths(graph, v, goal, on_path):\n    if v == goal:\n        return 1\n    on_path.add(v)\n    total = 0\n    for w in graph[v]:\n        if w not in on_path:\n            total += count_paths(graph, w, goal, on_path)\n    on_path.remove(v)\n    return total\n\n\ndef ladder(k):\n    graph = {v: [] for v in range(2 * k)}\n    for i in range(k):\n        graph[i].append(k + i)\n        graph[k + i].append(i)\n    for i in range(k - 1):\n        for v in [i, k + i]:\n            graph[v].append(v + 1)\n            graph[v + 1].append(v)\n    return graph\n\n\nfor k in [4, 8, 12, 16]:\n    print(k, \"rungs:\", count_paths(ladder(k), 0, 2 * k - 1, set()), \"paths\")\n```\n\n```output\n4 rungs: 8 paths\n8 rungs: 128 paths\n12 rungs: 2048 paths\n16 rungs: 32768 paths\n```\n\nA ladder of 60 rungs has 2⁵⁹ paths, over 500 million billion: no computer gets through them. Dijkstra takes each of its 120 vertices once.\n","task":"Fill in the blanks of Dijkstra's algorithm: taking the closest pair from the heap, recognizing a stale pair, and the distance that goes into the heap with a neighbor. `taken` lists the vertices whose edges were relaxed, so a stale pair that isn't skipped shows up twice. The program should print:\n\n```\n{'home': 0, 'bakery': 3, 'park': 1, 'school': 4}\n['home', 'park', 'bakery', 'school']\n```\n","kind":"fill","seed":"import heapq\n\n\ndef dijkstra(graph, start):\n    dist = {start: 0}\n    heap = [(0, start)]\n    taken = []\n    while heap:\n        d, v = [[heapq.heappop(heap)]]\n        if [[d > dist[v]‖d != dist[v]‖dist[v] < d]]:\n            continue\n        taken.append(v)\n        for w, cost in graph[v]:\n            if w not in dist or d + cost < dist[w]:\n                dist[w] = d + cost\n                heapq.heappush(heap, ([[d + cost]], w))\n    return dist, taken\n\n\nroads = {\n    \"home\": [(\"bakery\", 4), (\"park\", 1)],\n    \"park\": [(\"bakery\", 2), (\"school\", 6)],\n    \"bakery\": [(\"school\", 1)],\n    \"school\": [],\n}\ndist, taken = dijkstra(roads, \"home\")\nprint(dist)\nprint(taken)\n","solution":"import heapq\n\n\ndef dijkstra(graph, start):\n    dist = {start: 0}\n    heap = [(0, start)]\n    taken = []\n    while heap:\n        d, v = heapq.heappop(heap)\n        if d > dist[v]:\n            continue\n        taken.append(v)\n        for w, cost in graph[v]:\n            if w not in dist or d + cost < dist[w]:\n                dist[w] = d + cost\n                heapq.heappush(heap, (d + cost, w))\n    return dist, taken\n\n\nroads = {\n    \"home\": [(\"bakery\", 4), (\"park\", 1)],\n    \"park\": [(\"bakery\", 2), (\"school\", 6)],\n    \"bakery\": [(\"school\", 1)],\n    \"school\": [],\n}\ndist, taken = dijkstra(roads, \"home\")\nprint(dist)\nprint(taken)\n","hints":["`heapq.heappop(heap)` takes the pair with the smallest distance. A pair is stale when its distance is bigger than the best one known for its vertex: `d > dist[v]`.","The neighbor goes into the heap with the distance just stored for it, `d + cost`, first in the pair, so that the heap orders by distance."],"tests":[{"name":"Output","stdin":"","expect":"{'home': 0, 'bakery': 3, 'park': 1, 'school': 4}\n['home', 'park', 'bakery', 'school']","hidden":false}],"require":[],"forbid":[],"more":[{"task":"A country has n towns, numbered 0 to n - 1, and `roads` lists its roads as `(a, b, minutes)`: a two-way road between towns a and b that takes that many minutes. Write `cheapest(n, roads, start, goal)` that returns the fewest minutes it takes to drive from `start` to `goal`, or -1 when there's no way. No road takes a negative number of minutes.\n\nUse Dijkstra's algorithm, and stop as soon as the goal comes out of the heap. Trying every route is hopeless: one hidden test is a ladder of 60 rungs, with 2⁵⁹ routes between its corners, and another has 20,000 towns. A BFS that puts a town back in the queue each time its minutes go down gets the right answers too, but it can take O(V·E) steps, and a third hidden test, with 30,001 towns, is built to make it run out of time.\n\nFor example, from 0 to 4 the best way is 0, 1, 3, 2, 4: 2 + 4 + 1 + 6 = 13 minutes, though it takes four roads and 0, 3, 4 takes only two (9 + 10 = 19 minutes). So this program:\n\n```python\nroads = [(0, 3, 9), (0, 1, 2), (1, 3, 4), (1, 2, 7), (3, 2, 1), (2, 4, 6), (3, 4, 10)]\nprint(cheapest(5, roads, 0, 4))\nprint(cheapest(5, roads, 4, 1))\n```\n\nprints:\n\n```\n13\n11\n```\n","kind":"code","seed":"import heapq\n\n\ndef cheapest(n, roads, start, goal):\n    pass\n\n\nif __name__ == \"__main__\":\n    roads = [(0, 3, 9), (0, 1, 2), (1, 3, 4), (1, 2, 7), (3, 2, 1), (2, 4, 6), (3, 4, 10)]\n    print(cheapest(5, roads, 0, 4))\n    print(cheapest(5, roads, 4, 1))\n","solution":"import heapq\n\n\ndef cheapest(n, roads, start, goal):\n    graph = [[] for _ in range(n)]\n    for a, b, minutes in roads:\n        graph[a].append((b, minutes))\n        graph[b].append((a, minutes))\n    dist = [None] * n\n    dist[start] = 0\n    heap = [(0, start)]\n    while heap:\n        d, town = heapq.heappop(heap)\n        if town == goal:\n            return d\n        if d > dist[town]:\n            continue\n        for other, minutes in graph[town]:\n            if dist[other] is None or d + minutes < dist[other]:\n                dist[other] = d + minutes\n                heapq.heappush(heap, (d + minutes, other))\n    return -1\n\n\nif __name__ == \"__main__\":\n    roads = [(0, 3, 9), (0, 1, 2), (1, 3, 4), (1, 2, 7), (3, 2, 1), (2, 4, 6), (3, 4, 10)]\n    print(cheapest(5, roads, 0, 4))\n    print(cheapest(5, roads, 4, 1))\n","hints":["Build a weighted adjacency list: for each road, `(b, minutes)` goes into a's list and `(a, minutes)` into b's.","Keep the best known minutes for each town (a list of `None`s to start, with 0 for `start`) and a heap of `(minutes, town)` pairs. Pop the closest; if it's the goal, return its minutes, and if it's stale, skip it.","Otherwise relax each road from the town: when `d + minutes` beats the other town's best (or it has none), store it and push the pair. If the heap runs empty, the goal can't be reached: -1."],"tests":[{"name":"Test 1","stdin":"","call":"roads = [(0, 3, 9), (0, 1, 2), (1, 3, 4), (1, 2, 7), (3, 2, 1), (2, 4, 6), (3, 4, 10)]\nprint(cheapest(5, roads, 0, 4))\nprint(cheapest(5, roads, 4, 1))\n","expect":"13\n11","hidden":false},{"name":"cheapest(3, [(0, 1, 5)], 2, 2)","stdin":"","call":"cheapest(3, [(0, 1, 5)], 2, 2)\n","expect":"0","hidden":true,"returns":true},{"name":"cheapest(4, [(0, 1, 5), (2, 3, 1)], 0, 3)","stdin":"","call":"cheapest(4, [(0, 1, 5), (2, 3, 1)], 0, 3)\n","expect":"-1","hidden":true,"returns":true},{"name":"cheapest(4, [(0, 3, 50), (0, 1, 10), (1, 2, 10), (2, 3, 10)], 0, 3)","stdin":"","call":"cheapest(4, [(0, 3, 50), (0, 1, 10), (1, 2, 10), (2, 3, 10)], 0, 3)\n","expect":"30","hidden":true,"returns":true},{"name":"cheapest(3, [(0, 1, 0), (1, 2, 0), (0, 2, 1)], 2, 0)","stdin":"","call":"cheapest(3, [(0, 1, 0), (1, 2, 0), (0, 2, 1)], 2, 0)\n","expect":"0","hidden":true,"returns":true},{"name":"cheapest(2, [(0, 1, 5), (1, 0, 2)], 0, 1)","stdin":"","call":"cheapest(2, [(0, 1, 5), (1, 0, 2)], 0, 1)\n","expect":"2","hidden":true,"returns":true},{"name":"Test 7","stdin":"","call":"k = 60\nroads = [(i, i + 1, i * 7 % 10 + 1) for i in range(k - 1)]\nroads += [(k + i, k + i + 1, i * 3 % 10 + 1) for i in range(k - 1)]\nroads += [(i, k + i, i * 5 % 7 + 1) for i in range(k)]\nprint(cheapest(2 * k, roads, 0, 2 * k - 1))\n","expect":"295","hidden":true,"big":true},{"name":"Test 8","stdin":"","call":"n = 20_000\nroads = [(i, i + 1, i % 9 + 1) for i in range(n - 1)] + [(i, i * 37 % n, i % 50 + 5) for i in range(n)]\nprint(cheapest(n, roads, 0, n - 1), cheapest(n + 1, roads, 0, n))\n","expect":"43 -1","hidden":true,"big":true},{"name":"Test 9","stdin":"","call":"k = 30_000\nroads = [(0, j, 2 * (k - j) + 5) for j in range(1, k + 1)] + [(j, j + 1, 1) for j in range(1, k)]\nprint(cheapest(k + 1, roads, 0, 1))\n","expect":"30004","hidden":true,"big":true}],"require":[],"forbid":[]},{"task":"Each cell of a grid has a cost to enter. Write `cheapest_grid(costs)` that returns the smallest total cost of a way from the top left cell to the bottom right one, moving up, down, left or right, counting the cost of every cell on the way, both ends included. All costs are positive.\n\nThe grid is a graph whose vertices are the cells: run Dijkstra's algorithm with a heap of `(total, row, column)` and a dict of the best total for each cell. Moving only right and down isn't enough: the best way can go up or left. And trying every way is hopeless: a 10 by 10 grid already has about 41 billion billion of them, and the hidden tests have grids of 150 by 150.\n\nFor example, here the best way goes right twice and down twice, then left back to the first column, down to the bottom row and right along it: 2 + 1 + 1 + 1 + 2 + 1 + 1 + 1 + 2 + 1 + 1 + 1 + 3 = 18. Moving only right and down, the best you can do is 20. So this program:\n\n```python\ncosts = [\n    [2, 1, 1, 6, 3],\n    [8, 9, 1, 7, 4],\n    [1, 1, 2, 8, 2],\n    [1, 7, 9, 9, 1],\n    [2, 1, 1, 1, 3],\n]\nprint(cheapest_grid(costs))\n```\n\nprints:\n\n```\n18\n```\n","kind":"code","seed":"import heapq\n\n\ndef cheapest_grid(costs):\n    pass\n\n\nif __name__ == \"__main__\":\n    costs = [\n        [2, 1, 1, 6, 3],\n        [8, 9, 1, 7, 4],\n        [1, 1, 2, 8, 2],\n        [1, 7, 9, 9, 1],\n        [2, 1, 1, 1, 3],\n    ]\n    print(cheapest_grid(costs))\n","solution":"import heapq\n\n\ndef cheapest_grid(costs):\n    rows, cols = len(costs), len(costs[0])\n    best = {(0, 0): costs[0][0]}\n    heap = [(costs[0][0], 0, 0)]\n    while heap:\n        total, r, c = heapq.heappop(heap)\n        if (r, c) == (rows - 1, cols - 1):\n            return total\n        if total > best[(r, c)]:\n            continue\n        for nr, nc in [(r - 1, c), (r + 1, c), (r, c - 1), (r, c + 1)]:\n            if 0 <= nr < rows and 0 <= nc < cols:\n                new_total = total + costs[nr][nc]\n                if (nr, nc) not in best or new_total < best[(nr, nc)]:\n                    best[(nr, nc)] = new_total\n                    heapq.heappush(heap, (new_total, nr, nc))\n\n\nif __name__ == \"__main__\":\n    costs = [\n        [2, 1, 1, 6, 3],\n        [8, 9, 1, 7, 4],\n        [1, 1, 2, 8, 2],\n        [1, 7, 9, 9, 1],\n        [2, 1, 1, 1, 3],\n    ]\n    print(cheapest_grid(costs))\n","hints":["Start with `best = {(0, 0): costs[0][0]}` and the heap `[(costs[0][0], 0, 0)]`: the start cell's own cost counts.","Pop the smallest total. If its cell is the bottom right one, that total is the answer. If it's bigger than the best total stored for the cell, it's stale: skip it.","Otherwise look at the four neighbors inside the grid. Entering one costs `total + costs[nr][nc]`: when that beats the neighbor's best (or it has none), store it and push `(new_total, nr, nc)`."],"tests":[{"name":"Test 1","stdin":"","call":"costs = [\n    [2, 1, 1, 6, 3],\n    [8, 9, 1, 7, 4],\n    [1, 1, 2, 8, 2],\n    [1, 7, 9, 9, 1],\n    [2, 1, 1, 1, 3],\n]\nprint(cheapest_grid(costs))\n","expect":"18","hidden":false},{"name":"cheapest_grid([[5]])","stdin":"","call":"cheapest_grid([[5]])\n","expect":"5","hidden":true,"returns":true},{"name":"cheapest_grid([[2, 3, 4]])","stdin":"","call":"cheapest_grid([[2, 3, 4]])\n","expect":"9","hidden":true,"returns":true},{"name":"cheapest_grid([[1, 1, 5, 9], [9, 1, 1, 9], [9, 9, 1, 9], [9, 1, 1, 9], [9, 1, 9, 9], [9, 1, 1, 1]])","stdin":"","call":"cheapest_grid([[1, 1, 5, 9], [9, 1, 1, 9], [9, 9, 1, 9], [9, 1, 1, 9], [9, 1, 9, 9], [9, 1, 1, 1]])\n","expect":"11","hidden":true,"returns":true},{"name":"cheapest_grid([[1, 1, 1, 1, 1], [9, 9, 9, 9, 1], [1, 1, 1, 1, 1], [1, 9, 9, 9, 9], [1, 1, 1, 1, 1]])","stdin":"","call":"cheapest_grid([[1, 1, 1, 1, 1], [9, 9, 9, 9, 1], [1, 1, 1, 1, 1], [1, 9, 9, 9, 9], [1, 1, 1, 1, 1]])\n","expect":"17","hidden":true,"returns":true},{"name":"cheapest_grid([[(r * 7 + c * 3) % 5 + 1 for c in range(5)] for r in range(5)])","stdin":"","call":"cheapest_grid([[(r * 7 + c * 3) % 5 + 1 for c in range(5)] for r in range(5)])\n","expect":"17","hidden":true,"returns":true},{"name":"cheapest_grid([[(r * 5 + c * 11) % 7 + 1 for c in range(10)] for r in range(10)])","stdin":"","call":"cheapest_grid([[(r * 5 + c * 11) % 7 + 1 for c in range(10)] for r in range(10)])\n","expect":"59","hidden":true,"big":true,"returns":true},{"name":"cheapest_grid([[500 if (r % 4 == 1 and c < 149) or (r % 4 == 3 and c > 0) else 1 for c in range(150)] for r in range(150)])","stdin":"","call":"cheapest_grid([[500 if (r % 4 == 1 and c < 149) or (r % 4 == 3 and c > 0) else 1 for c in range(150)] for r in range(150)])\n","expect":"11325","hidden":true,"big":true,"returns":true},{"name":"cheapest_grid([[(r * 31 + c * 17) % 9 + 1 for c in range(150)] for r in range(150)])","stdin":"","call":"cheapest_grid([[(r * 31 + c * 17) % 9 + 1 for c in range(150)] for r in range(150)])\n","expect":"1049","hidden":true,"big":true,"returns":true}],"require":[],"forbid":[]}]}