Topics › Graphs › Shortest Path Algorithms and Problems
Dijkstra's algorithm
Java source ↗ Edit this article ↗
Jump to code:
Problem
You are given an undirected weighted graph with V vertices numbered 0 to V - 1, a list of edges {u, v, w} with non-negative weights w, and a source vertex src. Return an array whose i-th entry is the length of the shortest path from src to vertex i. The sheet’s version (GFG, “Implementing Dijkstra Algorithm”) passes an adjacency list and promises a connected graph; here the input is an edge list and any vertex that cannot be reached gets -1, so the code also works on disconnected graphs.
Constraints: weights are non-negative integers of moderate size (around 10^3), and V and E can be large enough that an O((V + E) log V) solution is expected.
Example 1
V = 3, src = 2, edges = [0-1 (1), 0-2 (6), 1-2 (3)]
answer = [4, 3, 0] // 2 → 1 → 0 costs 3 + 1 = 4, cheaper than the direct edge of 6
Example 2
V = 5, src = 0
edges = [0-1 (2), 0-2 (4), 1-2 (1), 1-3 (7), 2-4 (3), 3-4 (1)]
answer = [0, 2, 3, 7, 6] // vertex 3 via 0-1-2-4-3 = 2 + 1 + 3 + 1 = 7, not via the edge 1-3 (total 9)
Every approach uses the same two helpers: an adjacency list of {neighbour, weight} pairs (each undirected edge stored in both directions), and a final step that turns the “infinity” marker into -1.
static List<List<int[]>> buildAdj(int V, int[][] edges) {
List<List<int[]>> adj = new ArrayList<>();
for (int i = 0; i < V; i++) adj.add(new ArrayList<>());
for (int[] e : edges) {
adj.get(e[0]).add(new int[]{e[1], e[2]});
adj.get(e[1]).add(new int[]{e[0], e[2]});
}
return adj;
}
/** Turns the "infinity" marker into -1 for vertices that were never reached. */
static int[] finish(int[] dist) {
for (int i = 0; i < dist.length; i++) if (dist[i] == Integer.MAX_VALUE) dist[i] = -1;
return dist;
}
Approach 1: Brute force, try every simple path
Intuition. The shortest distance is, by definition, the minimum over all paths. With non-negative weights a shortest path never has to repeat a vertex: a repeated vertex means the path contains a cycle, and cutting out a cycle of weight ≥ 0 never makes the path longer. So it is enough to enumerate simple paths. A DFS from src that marks the vertices on the current path, records the arrival cost at each vertex, and unmarks on the way back does exactly that.
Algorithm.
- Set
best[v] = ∞for allv. explore(u, d): updatebest[u]withd, markuas on the path, recurse into every neighbour not on the path with costd + w, then unmarku.- Call
explore(src, 0)and convert∞to-1.
static int[] bruteForce(int V, int[][] edges, int src) {
List<List<int[]>> adj = buildAdj(V, edges);
int[] best = new int[V];
Arrays.fill(best, Integer.MAX_VALUE);
boolean[] onPath = new boolean[V];
explore(src, 0, adj, onPath, best);
return finish(best);
}
static void explore(int u, int d, List<List<int[]>> adj, boolean[] onPath, int[] best) {
best[u] = Math.min(best[u], d);
onPath[u] = true;
for (int[] e : adj.get(u)) {
if (!onPath[e[0]]) explore(e[0], d + e[1], adj, onPath, best);
}
onPath[u] = false; // backtrack so other paths may pass through u
}
Complexity. Time is exponential: a complete graph on V vertices has about (V - 2)! simple paths between two vertices, already millions for V = 12. Space O(V) for the recursion and the onPath array. The waste is that the DFS keeps re-exploring a vertex through different histories even though only the cheapest arrival at that vertex matters for what comes after.
Approach 2: Better, Dijkstra with an array
Intuition. Grow a set of settled vertices whose distance is final. At the start only src has a known distance, 0. The claim that drives the algorithm: among unsettled vertices, the one with the smallest tentative distance d can be settled immediately. Any other route to it must leave the settled region through some unsettled vertex x, whose tentative distance is already ≥ d, and every remaining edge on that route adds a weight ≥ 0, so the route costs at least d. After settling a vertex u, relax its edges: for each neighbour v, set dist[v] = min(dist[v], dist[u] + w).
This argument is the exact place where non-negative weights are needed. A negative edge after x could make the detour cheaper, which is why Dijkstra fails on negative weights.
Algorithm.
dist[src] = 0, everything else∞; no vertex is settled.- Repeat up to
Vtimes: scan for the unsettled vertexuwith the smallest finitedist. If none exists, stop (the rest is unreachable). - Settle
uand relax every edgeu - vwhose other end is unsettled.
static int[] better(int V, int[][] edges, int src) {
List<List<int[]>> adj = buildAdj(V, edges);
int[] dist = new int[V];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[src] = 0;
boolean[] settled = new boolean[V];
for (int iter = 0; iter < V; iter++) {
int u = -1;
for (int i = 0; i < V; i++) {
if (!settled[i] && dist[i] != Integer.MAX_VALUE && (u == -1 || dist[i] < dist[u])) u = i;
}
if (u == -1) break; // everything left is unreachable
settled[u] = true; // dist[u] can no longer improve
for (int[] e : adj.get(u)) {
int v = e[0];
if (!settled[v] && dist[u] + e[1] < dist[v]) dist[v] = dist[u] + e[1];
}
}
return finish(dist);
}
Complexity. Time O(V^2 + E): V scans of O(V) each, and every edge is relaxed a constant number of times. Space O(V + E). This is the right choice for very dense graphs, where E is close to V^2 and a heap would only add a log factor. For sparse graphs the repeated scan is the bottleneck.
Approach 3: Optimal, Dijkstra with a priority queue
Intuition. Replace the linear scan with a min-heap keyed by tentative distance, so the closest unsettled vertex comes out in O(log V). Java’s PriorityQueue has no “decrease key” operation, so whenever dist[v] improves we simply push a new entry {newDist, v} and leave the old one inside. When an entry pops whose distance is larger than the current dist[u], it is stale (a better entry for u was already processed), and we skip it. This is called lazy deletion.
Algorithm.
dist[src] = 0, push{0, src}.- While the heap is not empty: pop
{d, u}. Ifd > dist[u], skip it. - Otherwise relax each edge
u - v: ifd + w < dist[v], updatedist[v]and push{d + w, v}.
static int[] optimal(int V, int[][] edges, int src) {
List<List<int[]>> adj = buildAdj(V, edges);
int[] dist = new int[V];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[src] = 0;
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0])); // {distance, vertex}
pq.add(new int[]{0, src});
while (!pq.isEmpty()) {
int[] top = pq.poll();
int d = top[0], u = top[1];
if (d > dist[u]) continue; // stale entry: u was settled with a smaller distance
for (int[] e : adj.get(u)) {
int v = e[0], nd = d + e[1];
if (nd < dist[v]) {
dist[v] = nd;
pq.add(new int[]{nd, v});
}
}
}
return finish(dist);
}
Dry run on Example 2 (src = 0):
| Pop | Stale? | Improvements pushed | dist after |
|---|---|---|---|
| {0, 0} | no | 1 → 2, 2 → 4 | [0, 2, 4, ∞, ∞] |
| {2, 1} | no | 2 → 3, 3 → 9 | [0, 2, 3, 9, ∞] |
| {3, 2} | no | 4 → 6 | [0, 2, 3, 9, 6] |
| {4, 2} | yes, 4 > 3 | none | unchanged |
| {6, 4} | no | 3 → 7 | [0, 2, 3, 7, 6] |
| {7, 3} | no | none | unchanged |
| {9, 3} | yes, 9 > 7 | none | final [0, 2, 3, 7, 6] |
Complexity. Every successful relaxation pushes one entry, so the heap holds O(E) entries and each push or pop costs O(log E) = O(log V) (because E ≤ V^2). Total time O((V + E) log V), space O(V + E).
Approach 4: Optimal variant, a TreeSet with real deletion
Intuition. An ordered set can remove any element, so when dist[v] improves we erase the old pair {dist[v], v} before inserting the new one. The set never holds stale entries and has at most one entry per vertex. This is the C++ set version that is often taught alongside the heap version. The comparator must break ties by vertex number: if two vertices with equal distance compared as equal, TreeSet would treat them as duplicates and silently drop one.
static int[] optimalSet(int V, int[][] edges, int src) {
List<List<int[]>> adj = buildAdj(V, edges);
int[] dist = new int[V];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[src] = 0;
TreeSet<int[]> set = new TreeSet<>((a, b) -> a[0] != b[0] ? Integer.compare(a[0], b[0]) : Integer.compare(a[1], b[1]));
set.add(new int[]{0, src});
while (!set.isEmpty()) {
int[] top = set.pollFirst();
int d = top[0], u = top[1];
for (int[] e : adj.get(u)) {
int v = e[0], nd = d + e[1];
if (nd < dist[v]) {
if (dist[v] != Integer.MAX_VALUE) set.remove(new int[]{dist[v], v}); // drop the outdated entry
dist[v] = nd;
set.add(new int[]{nd, v});
}
}
}
return finish(dist);
}
Complexity. Also O((V + E) log V) time, O(V + E) space. In Java the heap version is usually faster in practice because PriorityQueue is an array-backed heap with small constants, while every TreeSet operation walks and rebalances a red-black tree.
Edge cases and pitfalls
- Negative weights. Dijkstra’s settling argument breaks. In an undirected graph a single negative edge is already a negative cycle (walk it back and forth), so shortest paths are undefined; for directed graphs with negative edges, use Bellman-Ford.
- Unreachable vertices. Never add a weight to the
Integer.MAX_VALUEmarker. The array version never picks a vertex at infinity, and the heap versions never push one, so the overflow cannot happen here. - Overflow. A distance can reach
(V - 1) · maxWeight. WithV = 10^5and weights up to10^4that is10^9, still anint; anything larger needslong. - Skipping the stale check. The answer stays correct without
if (d > dist[u]) continue;, but every stale pop relaxes all edges ofuagain, which can make the run much slower. - Zero-weight and parallel edges need no special handling; the tests include both.
- Directed graphs. Add each edge in one direction only; everything else is unchanged.
Related problems
- Djisktra’s Algorithm is the A2Z sheet’s version of the same problem.
- Why priority Queue is used in Djisktra’s Algorithm compares queue choices in more depth.
- Bellman ford algorithm handles negative edge weights and detects negative cycles.
- Network Delay Time is Dijkstra on a directed graph followed by a maximum.
- Path with minimum effort runs the same heap loop with a different cost function.