← All Posts
DSA Series · Graphs · Dijkstra’s Algorithm

Dijkstra’s Algorithm

Given a weighted graph with non-negative edge weights, how do you find the shortest path from a single source to every other vertex? Dijkstra’s algorithm answers this with an elegant greedy strategy: always process the closest unvisited vertex. In this post we cover the full algorithm with a priority-queue implementation, a detailed visual walkthrough, an interactive animation, why negative weights break it, clean C++ code, and competitive-programming patterns you should know.

Why Dijkstra’s Algorithm?

When edges carry non-negative weights, BFS no longer gives shortest paths. We need an algorithm that accounts for edge costs. Dijkstra’s algorithm solves the single-source shortest path problem by greedily extracting the nearest unvisited vertex and relaxing its neighbors.

Published by Edsger Dijkstra in 1959, it remains one of the most important graph algorithms. Applications include:

The Algorithm

Dijkstra maintains a distance array dist[] initialized to ∞ for all vertices except the source (which is 0). A priority queue (min-heap) always extracts the vertex with the smallest tentative distance.

Dijkstra’s Algorithm
dist[source] = 0
pq.push({0, source})

while pq is not empty:
    (d, u) = pq.extractMin()
    if d > dist[u]: continue   // stale entry
    for each edge (u, v, w):
        if dist[u] + w < dist[v]:
            dist[v] = dist[u] + w
            pq.push({dist[v], v})

Key insight: When we extract vertex u from the priority queue, dist[u] is finalized. This works because all edge weights are ≥ 0, so no future path through unvisited vertices can improve it.

Why Non-Negative Weights?

If an edge has negative weight, a vertex we already “finalized” might later find a shorter path through that negative edge. Dijkstra’s greedy assumption breaks.

Step-by-Step Walkthrough

Let’s trace Dijkstra on a 7-node weighted graph starting from node 0.

4 1 2 1 5 3 1 2 4 0 1 2 3 4 5 6

Initialize

dist = [0, ∞, ∞, ∞, ∞, ∞, ∞]. Push (0, node 0) into the priority queue. Node 0 is the source (orange).

4 1 2 1 5 3 1 2 4 0 1 2 3 4 5 6
PQ: {(0, node 0)}

Extract node 0 (dist=0)

Relax neighbors: dist[1] = 0+4 = 4, dist[2] = 0+1 = 1. Mark node 0 finalized (green).

4 1 2 1 5 3 1 2 4 0 1 2 3 4 5 6 dist = [0, 4, 1, ∞, ∞, ∞, ∞]
PQ: {(1, node 2), (4, node 1)}

Extract node 2 (dist=1) — Key relaxation!

Relax: dist[1] = min(4, 1+2) = 3improved from 4 to 3! And dist[4] = 1+5 = 6. The path 0→2→1 is cheaper than the direct edge 0→1.

4 1 2 1 5 3 1 2 4 0 1 2 3 4 5 6 dist = [0, 3, 1, ∞, 6, ∞, ∞]
PQ: {(3, node 1), (6, node 4)}

Extract node 1 (dist=3)

Relax: dist[3] = 3+1 = 4.

dist = [0, 3, 1, 4, 6, ∞, ∞]

Finalized: nodes 0, 2, 1
PQ: {(4, node 3), (6, node 4)}

Extract node 3 (dist=4)

Relax: dist[4] = min(6, 4+3) = 6 (no change), dist[5] = 4+1 = 5.

4 1 2 1 5 3 1 2 4 0 1 2 3 4 5 6 dist = [0, 3, 1, 4, 6, 5, ∞]
PQ: {(5, node 5), (6, node 4)}

Extract node 5 (dist=5)

Relax: dist[6] = 5+4 = 9.

dist = [0, 3, 1, 4, 6, 5, 9]

Finalized: node 5
PQ: {(6, node 4), (9, node 6)}

Extract node 4 (dist=6) — Another improvement!

Relax: dist[6] = min(9, 6+2) = 8improved from 9 to 8! The path 0→2→4→6 (cost 8) beats the path 0→2→1→3→5→6 (cost 9).

4 1 2 1 5 3 1 2 4 0 1 2 3 4 5 6 dist = [0, 3, 1, 4, 6, 5, 8]
PQ: {(8, node 6)}

Extract node 6 (dist=8) — Done!

No outgoing edges to relax. All nodes are finalized.

Final distances: [0, 3, 1, 4, 6, 5, 8]

✅ All nodes finalized!

Shortest paths: 0→2 (cost 1), 0→2→1 (cost 3), 0→2→1→3 (cost 4), 0→2→1→3→5 (cost 5), 0→2→4 (cost 6), 0→2→4→6 (cost 8).

Key observation: The direct edge 0→1 has weight 4, but the path 0→2→1 costs only 3. Dijkstra discovers this when processing node 2 and relaxes dist[1] from 4 down to 3. This is why we use a priority queue — processing node 2 (dist=1) before node 1 (dist=4) allows us to find the shorter path.

Why Negative Weights Break Dijkstra

Dijkstra’s greedy invariant says: “once a node is extracted from the priority queue, its distance is final.” Negative edges violate this because a longer initial path can become shorter after traversing a negative edge.

Counterexample

Consider this 4-node graph with one negative edge (dashed red):

1 5 −4 2 S A B C

Dijkstra’s execution:

  1. Extract S (dist=0). Relax: dist[A]=1, dist[B]=5.
  2. Extract A (dist=1). Mark A as finalized. Relax: dist[C]=1+2=3.
  3. Extract C (dist=3). Mark C as finalized.
  4. Extract B (dist=5). Relax B→C: dist[C] would become 5+(−4)=1, but C is already finalized with dist=3. Skipped!

Dijkstra’s answer: dist[C]=3 (via S→A→C). Correct answer: dist[C]=1 (via S→B→C = 5−4 = 1). The greedy assumption “extracted = optimal” fails because the cheaper path goes through a longer initial segment followed by a negative edge.

Rule of thumb: If any edge weight is negative, do not use Dijkstra. Use Bellman-Ford (O(V·E)) or SPFA instead. If there are no negative cycles, Bellman-Ford gives correct results. For negative cycles, Bellman-Ford detects them explicitly.

Interactive Animation

Step through Dijkstra’s algorithm on the 7-node weighted graph. Watch the priority queue and distance array update in real time.

Use Step for manual control, Play for auto-advance, or Reset to start over.

4 1 2 1 5 3 1 2 4 0 1 2 3 4 5 6 0
Unvisited Processing Finalized
dist[]:
0:0 1:∞ 2:∞ 3:∞ 4:∞ 5:∞ 6:∞

Press Play or Step to begin.

Priority Queue Variants & Optimizations

Binary Heap (Standard)

The standard implementation uses a binary min-heap (C++ priority_queue, Python heapq). Insert and extract-min are both O(log n). This gives O((V+E) log V) overall — the go-to for competitive programming.

Fibonacci Heap (Theoretical)

A Fibonacci heap supports decrease-key in O(1) amortized, reducing Dijkstra’s complexity to O(V log V + E). In practice, the constant factors are large, so binary heaps are faster for most graph sizes. Fibonacci heaps matter mainly for theoretical results and very dense graphs.

Lazy Deletion vs. Decrease-Key

Instead of implementing decrease-key (which binary heaps don’t natively support), we use lazy deletion: push a new (dist, v) pair and skip stale entries on extraction. This is simpler and performs well in practice, though the PQ can hold O(E) entries instead of O(V).

Dial’s Algorithm & 0-1 BFS

When all edge weights are small integers in [0, C], Dial’s algorithm uses an array of C+1 buckets instead of a heap, giving O(V·C + E). The special case where C=1 (all edges weight 0 or 1) leads to 0-1 BFS using a deque — push weight-0 edges to the front, weight-1 edges to the back. This runs in O(V+E).

Binary Heap (Practical)

  • O((V+E) log V)
  • Simple with lazy deletion
  • Best for sparse graphs
  • Used in 99% of CP solutions

Specialized Variants

  • Fibonacci: O(V log V + E), rarely practical
  • Dial: O(VC + E) for weights in [0,C]
  • 0-1 BFS: O(V+E) for weights 0/1
  • Indexed PQ: better constants, more code

C++ Implementation

Standard Dijkstra with Priority Queue

#include <bits/stdc++.h>
using namespace std;

vector<long long> dijkstra(int src, const vector<vector<pair<int,int>>>& adj) {
    int n = adj.size();
    vector<long long> dist(n, LLONG_MAX);
    priority_queue<pair<long long,int>, vector<pair<long long,int>>,
                   greater<pair<long long,int>>> pq;

    dist[src] = 0;
    pq.push({0, src});

    while (!pq.empty()) {
        auto [d, u] = pq.top(); pq.pop();
        if (d > dist[u]) continue; // stale entry

        for (auto [v, w] : adj[u]) {
            if (dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                pq.push({dist[v], v});
            }
        }
    }
    return dist;
}

With Path Reconstruction

pair<vector<long long>, vector<int>> dijkstra_path(int src,
    const vector<vector<pair<int,int>>>& adj) {
    int n = adj.size();
    vector<long long> dist(n, LLONG_MAX);
    vector<int> parent(n, -1);
    priority_queue<pair<long long,int>, vector<pair<long long,int>>,
                   greater<>> pq;

    dist[src] = 0;
    pq.push({0, src});

    while (!pq.empty()) {
        auto [d, u] = pq.top(); pq.pop();
        if (d > dist[u]) continue;
        for (auto [v, w] : adj[u]) {
            if (dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                parent[v] = u;
                pq.push({dist[v], v});
            }
        }
    }
    return {dist, parent};
}

// Reconstruct path from src to dst
vector<int> getPath(int dst, const vector<int>& parent) {
    vector<int> path;
    for (int v = dst; v != -1; v = parent[v])
        path.push_back(v);
    reverse(path.begin(), path.end());
    return path;
}

Complexity Analysis

Time Complexity

ImplementationTimeSpaceBest For
Binary Heap (lazy deletion)O((V+E) log V)O(V+E)General use, CP
Fibonacci HeapO(V log V + E)O(V+E)Dense graphs (theory)
Dial’s AlgorithmO(V·C + E)O(V·C)Small integer weights [0,C]
0-1 BFS (deque)O(V + E)O(V+E)Weights in {0, 1}
Adjacency MatrixO(V²)O(V²)Dense, small V

Why O((V+E) log V)?

Each vertex is extracted from the PQ at most once (stale entries are skipped in O(1)). Each edge is relaxed at most once, and each relaxation involves one PQ insertion costing O(log V). Total: V extractions × O(log V) + E insertions × O(log V) = O((V+E) log V).

Space Complexity

O(V + E) for the adjacency list and distance array. The priority queue can hold up to O(E) entries with lazy deletion (each edge relaxation may push one entry). In practice, this is rarely a bottleneck.

For most competitive programming problems: V, E ≤ 105–106, so the binary heap version is fast enough.

Competitive Programming Patterns

Dijkstra appears in many disguises in competitive programming. Here are the most common patterns:

1. Shortest Path on Grid

Many CP problems give a 2D grid where each cell has a cost. Model it as a graph: each cell is a node, edges go to 4 (or 8) neighbors with the neighbor’s cost as edge weight. Run Dijkstra from top-left to bottom-right. If costs are 0 or 1, prefer 0-1 BFS with a deque for O(V+E) performance.

2. Multi-Source Dijkstra

Need shortest distance from any of several source nodes? Push all sources into the PQ with dist=0 at the start. The algorithm runs identically — it’s equivalent to adding a virtual super-source with zero-weight edges to all real sources. Used in problems like “distance to nearest hospital” or “escape fire.”

3. Modified Edge Weights

Some problems require transforming edge weights. Example: “maximize the minimum edge on a path” — negate weights and use Dijkstra, or modify the relaxation condition. Another example: “path with maximum probability” — use a max-heap and multiply probabilities instead of adding costs.

4. Dijkstra on States (State-Space Expansion)

Add extra dimensions: dist[node][state]. Example: “shortest path using at most K toll roads” — state = (node, tolls_used). The graph expands from V nodes to V×K nodes. Example: “you can teleport once” — state = (node, teleported?). This is an extremely powerful technique for handling constraints.

5. Bidirectional Dijkstra

For single-pair shortest paths, run Dijkstra simultaneously from source and target. Stop when the two search frontiers meet. Approximately 2× faster in practice. Real-world navigation systems (e.g., Contraction Hierarchies) build on this idea for even faster queries.

Practice Problems

Test your understanding with these problems, ordered roughly by difficulty:

Network Delay Time (LeetCode 743)

Direct application of Dijkstra. Find the time for a signal to reach all nodes — the answer is the maximum distance in the dist array.

Shortest Routes I (CSES)

Standard Dijkstra template problem. Great for verifying your implementation is correct and fast enough.

Path With Minimum Effort (LeetCode 1631)

Dijkstra on a grid where the “weight” is the absolute height difference between adjacent cells. Minimize the maximum edge weight on the path.

Cheapest Flights Within K Stops (LeetCode 787)

State-space Dijkstra: state = (cost, node, stops_remaining). Classic example of expanding the state for constrained shortest paths.

Dijkstra? (Codeforces 20C)

Classic Dijkstra with path reconstruction. Find the shortest path from node 1 to node n and print the actual path.

Investigation (CSES)

Find shortest distance, count shortest paths, and find min/max edges on any shortest path. Requires augmenting Dijkstra with extra bookkeeping.

Paths and Trees (Codeforces 545E)

Find a minimum-cost subgraph that forms a shortest path tree from a given source. Combines Dijkstra with greedy edge selection.

Summary

Key takeaways from this post:

  1. Dijkstra’s algorithm finds shortest paths from a single source in graphs with non-negative edge weights using a greedy, priority-queue-based approach.
  2. The core operation is relaxation: for each edge u→v(w), check if dist[u]+w improves dist[v].
  3. Lazy deletion (skip stale PQ entries) is simpler and faster in practice than decrease-key.
  4. Negative weights break Dijkstra because the greedy invariant (“extracted = finalized”) no longer holds. Use Bellman-Ford instead.
  5. Time complexity is O((V+E) log V) with a binary heap — the standard for competitive programming.
  6. For CP, know these patterns: grid shortest paths, multi-source Dijkstra, state-space expansion, and 0-1 BFS for weights in {0, 1}.
Practice tip: Most shortest-path problems in competitive programming can be solved with Dijkstra + one twist (modified weights, expanded state, multi-source). Master the basic template, then learn to adapt the state and edge-weight definitions for each problem.