Grid Algorithms· 25 min

Dijkstra's Algorithm

Priority queue based — shortest path with weights

Setup

0 walls · 15×30 grid
Long straight corridors with single gaps. Clean, structured, easy to follow.
Step 1 / 683

Current step

1 / 683

Initialize Dijkstra. Start=7,4, End=7,25.

Step log

Tip: pick a tool, then click or drag on the grid. Walls block the path; start and end are draggable. Hit Generate maze for a structured layout.

Start
End
Wall
Visited
Frontier
Current
Path
dijkstra.java
1List<Cell> dijkstra(Cell[][] grid, Cell start, Cell end) {
2 Map<Cell, Integer> dist = new HashMap<>();
3 dist.put(start, 0);
4 PriorityQueue<Cell> pq = new PriorityQueue<>(Comparator.comparingInt(dist::get));
5 pq.add(start);
6 Map<Cell, Cell> parent = new HashMap<>();
7 Set<Cell> visited = new HashSet<>();
8 while (!pq.isEmpty()) {
9 Cell cur = pq.poll();
10 if (!visited.add(cur)) continue;
11 if (cur.equals(end)) return reconstruct(parent, end);
12 for (Cell n : neighbors(cur)) {
13 int nd = dist.get(cur) + 1;
14 if (nd < dist.getOrDefault(n, Integer.MAX_VALUE)) {
15 dist.put(n, nd); parent.put(n, cur);
16 pq.add(n);
17 }
18 }
19 }
20 return null;
21}
● Java

Dijkstra

Treats every step as cost 1 and uses a priority queue (min-heap) to always expand the closest unvisited cell. Guarantees the shortest path.

Time
O((V + E) log V)
Visited
0
Frontier
1
Path length
0

Written guide

Dijkstra's Algorithm: complete guide

Dijkstra's algorithm finds the cheapest path from one source to every node in a graph with non-negative edge weights by always settling the closest unsettled node next. Here is the min-heap version with lazy deletion, why a single negative edge breaks it, and how it compares with Bellman-Ford and A*.

Intuition

Suppose you are planning drives out of your home city and every road has a travel time. The nearest town to you is easy: no other route can beat driving straight there, because any detour would start with a road at least as long. Once that town is settled, the next-closest town is either a direct neighbor of home or a neighbor of the town you just settled. Dijkstra's algorithm repeats that argument: always settle the unsettled node with the smallest known distance, then use it to improve the estimates of its neighbors.

The argument only works because roads never have negative times. Adding an edge can never make a path shorter, so once the cheapest frontier node is chosen, nothing discovered later can undercut it. A min-heap (priority queue) hands us that cheapest frontier node in O(log n) time instead of scanning every node.

How it works step by step

  1. Set dist[src] = 0 and every other distance to infinity. Push (0, src) onto a min-heap ordered by distance.
  2. Pop the smallest (d, u) from the heap.
  3. If d > dist[u], this entry is stale: a shorter route to u was already found and processed. Skip it. This is lazy deletion.
  4. Otherwise u is settled. For each edge u → v with weight w, compute nd = d + w.
  5. If nd < dist[v], relax the edge: set dist[v] = nd, record prev[v] = u, and push (nd, v). The old entry for v stays in the heap and will be skipped later.
  6. Repeat until the heap is empty. If you only need one target, you can stop the first time it is popped.

Dry run on a weighted directed graph

Edges A→B (4), A→C (1), C→B (2), B→D (1), C→D (5). Source A. The heap column lists entries in priority order after the action.

StepHeap (dist, node)Actiondist A, B, C, D
0[(0,A)]Initialise0, inf, inf, inf
1[(1,C), (4,B)]Pop A; relax B to 4 and C to 10, 4, 1, inf
2[(3,B), (4,B), (6,D)]Pop C; B improves to 1 + 2 = 3, D becomes 1 + 5 = 60, 3, 1, 6
3[(4,B), (4,D), (6,D)]Pop (3,B); D improves to 3 + 1 = 40, 3, 1, 4
4[(4,D), (6,D)]Pop (4,B); 4 > dist[B] = 3, stale, skipunchanged
5[(6,D)]Pop (4,D); D settled, no outgoing edgesunchanged
6[]Pop (6,D); stale, skip. Heap empty0, 3, 1, 4

Following prev from D gives D ← B ← C ← A, so the cheapest route is A → C → B → D with cost 4, even though the direct-looking A → B → D costs 5. The two stale entries in steps 4 and 6 are the price of lazy deletion: cheap to leave behind, cheap to skip.

Code

Dijkstra.java
import java.util.*;

public class Dijkstra {
    // adj.get(u) holds edges as {v, weight}; every weight must be >= 0.
    public static long[] shortestPaths(List<List<int[]>> adj, int src) {
        int n = adj.size();
        long[] dist = new long[n];
        Arrays.fill(dist, Long.MAX_VALUE);
        dist[src] = 0;
        PriorityQueue<long[]> pq = new PriorityQueue<>(Comparator.comparingLong(e -> e[0]));
        pq.offer(new long[]{0, src});

        while (!pq.isEmpty()) {
            long[] top = pq.poll();
            long d = top[0];
            int u = (int) top[1];
            if (d > dist[u]) continue;            // stale entry (lazy deletion)
            for (int[] edge : adj.get(u)) {
                int v = edge[0];
                long nd = d + edge[1];
                if (nd < dist[v]) {
                    dist[v] = nd;
                    pq.offer(new long[]{nd, v});
                }
            }
        }
        return dist;                              // Long.MAX_VALUE = unreachable
    }
}
dijkstra.py
import heapq


def dijkstra(graph, src):
    """graph maps node -> list of (neighbor, weight). Returns (dist, prev)."""
    dist = {node: float("inf") for node in graph}
    prev = {src: None}
    dist[src] = 0
    heap = [(0, src)]
    while heap:
        d, u = heapq.heappop(heap)
        if d > dist[u]:
            continue                      # stale entry
        for v, w in graph[u]:
            nd = d + w
            if nd < dist[v]:
                dist[v] = nd
                prev[v] = u
                heapq.heappush(heap, (nd, v))
    return dist, prev


def path_to(prev, target):
    if target not in prev:
        return []
    path = []
    while target is not None:
        path.append(target)
        target = prev[target]
    return path[::-1]


graph = {"A": [("B", 4), ("C", 1)], "B": [("D", 1)], "C": [("B", 2), ("D", 5)], "D": []}
dist, prev = dijkstra(graph, "A")
print(dist)                  # {'A': 0, 'B': 3, 'C': 1, 'D': 4}
print(path_to(prev, "D"))    # ['A', 'C', 'B', 'D']

Time and space complexity

VariantTimeSpace
Binary heap, lazy deletionO((V + E) log V)O(V + E), since the heap can hold one entry per relaxation
Binary heap with decrease-keyO((V + E) log V)O(V)
Array scan, no heapO(V^2)O(V); competitive on very dense graphs
Fibonacci heap (theoretical)O(E + V log V)O(V); rarely used in practice

With lazy deletion the heap can hold up to E entries, so each operation costs O(log E). Because E is at most V squared, log E is at most 2 log V, which is why the bound is still written as O((V + E) log V).

Edge cases

  • Unreachable nodes keep distance infinity; convert to -1 only at the end if the problem asks for it.
  • Zero-weight edges are fine. Only negative weights break correctness.
  • Parallel edges between the same pair are handled naturally; only the cheapest one wins a relaxation.
  • Overflow: in Java, adding to Integer.MAX_VALUE wraps negative. Use long distances or check for infinity before adding.
  • Undirected graphs: add every edge in both directions when building the adjacency list.

Why negative edges break Dijkstra

Take edges A→B (2), A→C (3), C→B (-2). Dijkstra pops A, then pops B at distance 2 and declares it settled. Later it pops C at 3 and discovers A → C → B costs 1, but B was already finalised, and anything computed from B's old value is now wrong. The greedy guarantee assumed no future edge could lower a settled distance, and a negative weight does exactly that. Bellman-Ford fixes this by relaxing every edge V - 1 times in O(V x E), and a further pass that still improves something proves a negative cycle exists. **A* search** goes the other direction: it keeps Dijkstra's non-negative assumption but adds a heuristic estimate of the remaining distance to the target, so on maps and grids it settles far fewer nodes. Explore it in the A* search lesson.

Common mistakes

  • Forgetting the if d > dist[u]: continue check. The result is still correct, but stale entries re-relax neighbors and blow up the running time.
  • Marking a node as visited when it is pushed instead of when it is popped. Unlike BFS, a node's first discovery is not necessarily its cheapest.
  • Pushing (node, dist) instead of (dist, node) into Python's heapq, which then orders by node id.
  • Using Dijkstra on a graph where all weights are 1. Plain BFS gives the same answer without the log factor.
  • Assuming it handles extra constraints like at most K stops. Those need the constraint inside the state, or Bellman-Ford limited to K rounds.

Variations and when to use it

Use Dijkstra for single-source cheapest paths with non-negative costs: network latency, travel time, grid movement where cells have different costs. The same loop solves minimax problems such as path with minimum effort by replacing d + w with max(d, w), and maximum probability paths by using a max-heap and multiplying. When the graph is unweighted, prefer Graph BFS. When edge weights are only 0 or 1, the 0-1 BFS deque is linear time.

  • #743 Network Delay Time — run Dijkstra from the source and return the largest finite distance.
  • #1514 Path with Maximum Probability — max-heap version where relaxing means multiplying probabilities.
  • #1631 Path With Minimum Effort — grid Dijkstra where a path's cost is its largest single step.
  • #778 Swim in Rising Water — minimax Dijkstra on a grid of elevations.
  • #1976 Number of Ways to Arrive at Destination — Dijkstra that also counts how many shortest paths reach each node.
  • #787 Cheapest Flights Within K Stops — shows the limit of plain Dijkstra; add stops to the state or use bounded Bellman-Ford.
  • #1368 Minimum Cost to Make at Least One Valid Path in a Grid — 0-1 weights, solvable with Dijkstra or a deque.

FAQ

›Why doesn't Dijkstra's algorithm work with negative weights?

It finalises a node the first time it is popped, assuming no later path can be cheaper. A negative edge discovered later can make an already-settled node cheaper, so the answer becomes wrong.

›What is lazy deletion in Dijkstra?

Instead of updating an entry's priority inside the heap, you push a new entry with the better distance and skip any popped entry whose distance is larger than the recorded best.

›What is the time complexity of Dijkstra with a priority queue?

O((V + E) log V) with a binary heap. Each relaxation may push once and each push or pop costs a logarithmic amount of work.

›When should I use Bellman-Ford instead?

When the graph has negative edge weights, when you need to detect a negative cycle, or when the path length in edges is capped, as in K-stop flight problems.

›Is A* better than Dijkstra?

For a single target with a good admissible heuristic, A* usually explores far fewer nodes. Dijkstra is A* with a heuristic of zero, and it is the right tool when you need distances to every node.