Topics › Graphs › Shortest Path Algorithms and Problems
Shortest path in DAG
Java source ↗ Edit this article ↗
Jump to code:
Problem
You are given a directed acyclic graph (DAG) with n nodes numbered 0 to n - 1 and a list of weighted edges, where {u, v, w} is an edge from u to v of weight w. Return the shortest distance from the source node (node 0 in the usual statement; the code takes src as a parameter) to every node. If a node cannot be reached, its distance is -1.
Constraints: 1 ≤ n ≤ 100, 1 ≤ m ≤ 100 edges, 0 ≤ w ≤ 100 in the original statement. The graph is guaranteed to have no directed cycle.
Example 1
n = 6
edges = [0→1 (2), 0→4 (1), 4→5 (4), 4→2 (2), 1→2 (3), 2→3 (6), 5→3 (1)]
answer = [0, 2, 3, 6, 1, 5]
// node 2: 0→4→2 costs 3, cheaper than 0→1→2 at 5
// node 3: 0→4→5→3 costs 6, cheaper than 0→4→2→3 at 9
Example 2
n = 4, edges = [0→1 (2), 0→2 (1)]
answer = [0, 2, 1, -1] // nothing leads to node 3
Approach 1: Brute force, Bellman-Ford relaxation
Intuition. A correct distance array satisfies dist[v] ≤ dist[u] + w for every edge u → v. Start with dist[src] = 0 and infinity elsewhere, and repeatedly relax edges: whenever an edge breaks the rule, lower dist[v]. After the r-th sweep over all edges, every node whose shortest path has at most r edges is correct, because each sweep extends correct prefixes by one more edge. A shortest path has at most n - 1 edges, so n - 1 sweeps suffice. This is Bellman-Ford, and it works on any graph without negative cycles, not just DAGs.
Algorithm.
- Fill
distwith infinity and setdist[src] = 0. - Repeat up to
n - 1times: relax every edge whose tail has a finite distance. Stop early if a sweep changes nothing. - Convert remaining infinities to
-1.
static int[] bruteForce(int n, int[][] edges, int src) {
final int INF = Integer.MAX_VALUE;
int[] dist = new int[n];
Arrays.fill(dist, INF);
dist[src] = 0;
for (int round = 1; round < n; round++) {
boolean changed = false;
for (int[] e : edges) {
if (dist[e[0]] != INF && dist[e[0]] + e[2] < dist[e[1]]) {
dist[e[1]] = dist[e[0]] + e[2];
changed = true;
}
}
if (!changed) break;
}
for (int i = 0; i < n; i++) if (dist[i] == INF) dist[i] = -1;
return dist;
}
Complexity. Time O(V × E): up to V - 1 sweeps over every edge. Space O(V) for the distances.
The waste is in the order. A sweep may relax the edges leaving u while dist[u] is still too large, so the work has to be redone in later sweeps once dist[u] improves.
Approach 2: Optimal, relax in topological order
Intuition. A DAG has a topological order: a list of the nodes in which every edge points from left to right. Process the nodes in that order and relax each node’s outgoing edges exactly once. When we reach u, every edge into u starts at a node that appears earlier in the list, so it has already been relaxed. dist[u] therefore already accounts for every way of arriving at u, it is final, and relaxing out of it once is enough.
More formally, take any shortest path src = x0 → x1 → ... → xk. Its nodes appear in increasing topological position. By induction, when x(i) is processed its distance is correct, and relaxing x(i) → x(i+1) makes dist[x(i+1)] correct before x(i+1) is processed.
So the whole algorithm is “topological sort, then one sweep”. We get the order from Kahn’s algorithm. The topo array doubles as Kahn’s queue, with head and tail as the queue’s ends. A DFS-based topological sort (push a node onto a stack after its descendants finish, then pop) works equally well.
Algorithm.
- Build the adjacency list of
{target, weight}pairs and the in-degrees. - Run Kahn’s algorithm to fill
topo. - Set
dist[src] = 0, all others infinity. - For each
uintopoorder: skip it ifdist[u]is infinity (unreachable fromsrc), otherwise relax every edgeu → v. - Convert remaining infinities to
-1.
static int[] optimal(int n, int[][] edges, int src) {
List<List<int[]>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
int[] indegree = new int[n];
for (int[] e : edges) {
adj.get(e[0]).add(new int[]{e[1], e[2]});
indegree[e[1]]++;
}
int[] topo = new int[n]; // used as the queue of Kahn's algorithm
int head = 0, tail = 0;
for (int i = 0; i < n; i++) if (indegree[i] == 0) topo[tail++] = i;
while (head < tail) {
int u = topo[head++];
for (int[] e : adj.get(u)) {
if (--indegree[e[0]] == 0) topo[tail++] = e[0];
}
}
final int INF = Integer.MAX_VALUE;
int[] dist = new int[n];
Arrays.fill(dist, INF);
dist[src] = 0;
for (int u : topo) { // all edges into u were relaxed before we get here
if (dist[u] == INF) continue; // u is not reachable from src
for (int[] e : adj.get(u)) {
dist[e[0]] = Math.min(dist[e[0]], dist[u] + e[1]);
}
}
for (int i = 0; i < n; i++) if (dist[i] == INF) dist[i] = -1;
return dist;
}
Dry run on Example 1. Kahn’s algorithm gives the order [0, 1, 4, 5, 2, 3]. Relaxing in that order (v ← d means dist[v] is lowered to d):
| u | dist[u] | relaxations | dist after (0..5) |
|---|---|---|---|
| 0 | 0 | 1 ← 2, 4 ← 1 | 0, 2, ∞, ∞, 1, ∞ |
| 1 | 2 | 2 ← 5 | 0, 2, 5, ∞, 1, ∞ |
| 4 | 1 | 5 ← 5, 2 ← 3 | 0, 2, 3, ∞, 1, 5 |
| 5 | 5 | 3 ← 6 | 0, 2, 3, 6, 1, 5 |
| 2 | 3 | 3: 9 is not better | 0, 2, 3, 6, 1, 5 |
| 3 | 6 | none | 0, 2, 3, 6, 1, 5 |
Node 2 was first set to 5 through node 1 and corrected to 3 through node 4, and both happened before node 2 itself was processed, which is what the topological order guarantees.
Complexity. Time O(V + E): Kahn’s algorithm is linear, and the relaxation sweep touches each edge once. Space O(V + E) for the adjacency list, the in-degrees, topo and dist.
Why not Dijkstra?
Dijkstra’s algorithm would also work for non-negative weights, but it costs O((V + E) log V) because of the priority queue. The topological sweep is linear. More importantly, the DAG method does not care about the sign of the weights. Dijkstra declares a node final when it is the closest unfinished node, which a later negative edge can contradict. In a DAG no such surprise is possible: everything that could lower dist[u] comes earlier in the order. The test with edges 0→1 (5), 0→2 (2), 2→1 (-4) returns [0, -2, 2]. If you allow negative weights, though, pick an unreachable marker other than -1, since -1 could then be a genuine distance.
Longest paths are just as easy in a DAG: negate the weights, or use max instead of min. In a general graph that problem is NP-hard.
Edge cases and pitfalls
- Unreachable nodes. Nodes that come before the source in topological order, or simply have no path from it, keep infinity and become
-1. The test withsrc = 4leaves nodes0and1at-1. - Overflow.
Integer.MAX_VALUE + wwraps to a negative number. Thedist[u] == INFskip (and the!= INFguard in the brute force) prevents relaxing from an unreachable node. - Source not first in the order. Node
3in the last test has in-degree0and is placed before the source. It must be skipped, not treated as distance0. - Single node.
n = 1returns[0]. - Not a DAG. If the input had a cycle, Kahn’s algorithm would leave some nodes out of
topo(the unused tail of the array stays0, so the sweep would be wrong). This problem guarantees a DAG; for general graphs use Dijkstra or Bellman-Ford.
Related problems
- Topo Sort gives the DFS way to produce the order.
- Topological sort or Kahn’s algorithm is the queue-based order used here.
- Shortest path in undirected graph with unit weights solves the unweighted case with BFS.
- Djisktra’s Algorithm handles general graphs with non-negative weights.
- Bellman Ford Algorithm is Approach 1 in full, including negative-cycle detection.