DAG algorithms
On a DAG the topological order turns hard-looking path problems into one linear pass: longest paths, shortest paths (even with negative weights) and path counts all follow from a dynamic program over the order.
Learning goals
- Compute single-source longest and shortest paths on a DAG in O(n + m)
- Explain why negative weights are no problem on a DAG
- Count paths between two vertices
- Recognise a DP over a topological order
Prerequisites
On a general graph, shortest paths need Dijkstra or Bellman-Ford and longest simple paths are intractable. If the graph has no cycle, the vertices can be processed in an order where everything a vertex depends on is already finished, and each of these problems becomes a short loop.
Longest path in DAG
Let best[v] be the longest path from the source to v. Process vertices in topological order; for each arrow u → v of weight w set best[v] = max(best[v], best[u] + w). When v is reached, every arrow into it has already been used, so its value is final. Total time O(n + m).
On a general graph the longest path is NP-hard, because a cycle lets a path grow without limit; the absence of cycles is what makes this easy. This is the critical-path computation from the previous lesson. Choose *Longest path from the source* in the animation.
1static long[] dagLongest(int n, int[][] edges, int s) { // edges {u, v, w}; Long.MIN_VALUE = unreachable2int[] order = topo(n, edges);3long[] best = new long[n];4Arrays.fill(best, Long.MIN_VALUE);5best[s] = 0;6for (int u : order) {7if (best[u] == Long.MIN_VALUE) continue;8for (int[] e : edges)9if (e[0] == u && best[u] + e[2] > best[e[1]]) best[e[1]] = best[u] + e[2];10}11return best;12}13static int[] topo(int n, int[][] edges) { // Kahn's order of a DAG14int[] indeg = new int[n], order = new int[n];15for (int[] e : edges) indeg[e[1]]++;16Deque<Integer> q = new ArrayDeque<>();17for (int v = 0; v < n; v++) if (indeg[v] == 0) q.add(v);18int k = 0;19while (!q.isEmpty()) {20int u = q.poll();21order[k++] = u;22for (int[] e : edges) if (e[0] == u && --indeg[e[1]] == 0) q.add(e[1]);23}24return order;25}
Dynamic programming over a topological order: longest, shortest, count of paths
Interactive · edit the inputdirected graph · 6 vertices · 9 edges
d=0
topological order
best[v]
A DAG has a topological order: 0 → 1 → 2 → 3 → 5 → 4. Processing vertices in that order means every arrow into a vertex has been handled before the vertex is used, so its value is final. The source starts at 0.
1long[] dagBest(int n, List<int[]>[] adj, int[] order, int s, boolean longest) { // adj[u] = {v, weight}2long NONE = longest ? Long.MIN_VALUE : Long.MAX_VALUE;3long[] best = new long[n]; Arrays.fill(best, NONE);4best[s] = 0;5for (int u : order) { // topological order: best[u] is final when u is reached6if (best[u] == NONE) continue; // not reachable from s7for (int[] e : adj[u]) {8long cand = best[u] + e[1];9if (best[e[0]] == NONE || (longest ? cand > best[e[0]] : cand < best[e[0]])) best[e[0]] = cand;10}11}12return best;13}14long[] countPaths(int n, List<int[]>[] adj, int[] order, int s) {15long[] ways = new long[n]; ways[s] = 1;16for (int u : order) for (int[] e : adj[u]) ways[e[0]] += ways[u]; // paths into u extend along every arrow out of u17return ways;18}
Live state
order
[0, 1, 2, 3, 5, 4]
source
0
Execution trace (dry run table)Show 11 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | order=[0, 1, 2, 3, 5, 4] source=0 | A DAG has a topological order: 0 → 1 → 2 → 3 → 5 → 4. Processing vertices in that order means every arrow into a vertex has been handled before the vertex is used, so its value is final. The source starts at 0. |
| 2 | L9 | u=0 v=1 cand=5 best[v]=5 | 0 → 1: 0 + 5 = 5. That beats the old value, so best[1] = 5. |
| 3 | L9 | u=0 v=2 cand=3 best[v]=3 | 0 → 2: 0 + 3 = 3. That beats the old value, so best[2] = 3. |
| 4 | L9 | u=1 v=3 cand=11 best[v]=11 | 1 → 3: 5 + 6 = 11. That beats the old value, so best[3] = 11. |
| 5 | L9 | u=1 v=2 cand=7 best[v]=7 | 1 → 2: 5 + 2 = 7. That beats the old value, so best[2] = 7. |
| 6 | L9 | u=2 v=4 cand=11 best[v]=11 | 2 → 4: 7 + 4 = 11. That beats the old value, so best[4] = 11. |
| 7 | L9 | u=2 v=5 cand=9 best[v]=9 | 2 → 5: 7 + 2 = 9. That beats the old value, so best[5] = 9. |
| 8 | L9 | u=2 v=3 cand=14 best[v]=14 | 2 → 3: 7 + 7 = 14. That beats the old value, so best[3] = 14. |
| 9 | L9 | u=3 v=5 cand=15 best[v]=15 | 3 → 5: 14 + 1 = 15. That beats the old value, so best[5] = 15. |
| 10 | L9 | u=3 v=4 cand=15 best[v]=15 | 3 → 4: 14 + 1 = 15. That beats the old value, so best[4] = 15. |
| 11 | L11 | return=[0, 5, 7, 14, 15, 15] arrows relaxed=9 | Values: [0, 5, 7, 14, 15, 15] ('-' = not reachable). Every arrow was used exactly once, so the whole computation is O(n + m). The longest path ends at 4 with length 15: 0 → 1 → 2 → 3 → 4 (read back through the parent links). |
Shortest path in DAG
The same pass with min gives shortest paths. Dijkstra needs non-negative weights because it settles a vertex before it has seen every route to it. The DAG order guarantees every route into a vertex has been examined before the vertex is used, so **negative weights are fine** here, and no priority queue is needed.
That makes it O(n + m) instead of Dijkstra's O((n + m) log n). Vertices that come before the source in the order, or cannot be reached from it, simply keep "unreachable"; the *Vertices before the source are unreachable* preset shows this.
1static long[] dagShortest(int n, int[][] edges, int s) { // edges {u, v, w}, negative w allowed; Long.MAX_VALUE = unreachable2int[] indeg = new int[n], order = new int[n];3for (int[] e : edges) indeg[e[1]]++;4Deque<Integer> q = new ArrayDeque<>();5for (int v = 0; v < n; v++) if (indeg[v] == 0) q.add(v);6int k = 0;7while (!q.isEmpty()) {8int u = q.poll();9order[k++] = u;10for (int[] e : edges) if (e[0] == u && --indeg[e[1]] == 0) q.add(e[1]);11}12long[] dist = new long[n];13Arrays.fill(dist, Long.MAX_VALUE);14dist[s] = 0;15for (int u : order) {16if (dist[u] == Long.MAX_VALUE) continue;17for (int[] e : edges)18if (e[0] == u && dist[u] + e[2] < dist[e[1]]) dist[e[1]] = dist[u] + e[2];19}20return dist;21}
Dynamic programming over a topological order: longest, shortest, count of paths
Interactive · edit the inputdirected graph · 6 vertices · 9 edges
d=0
topological order
best[v]
A DAG has a topological order: 0 → 1 → 2 → 3 → 5 → 4. Processing vertices in that order means every arrow into a vertex has been handled before the vertex is used, so its value is final. The source starts at 0.
1long[] dagBest(int n, List<int[]>[] adj, int[] order, int s, boolean longest) { // adj[u] = {v, weight}2long NONE = longest ? Long.MIN_VALUE : Long.MAX_VALUE;3long[] best = new long[n]; Arrays.fill(best, NONE);4best[s] = 0;5for (int u : order) { // topological order: best[u] is final when u is reached6if (best[u] == NONE) continue; // not reachable from s7for (int[] e : adj[u]) {8long cand = best[u] + e[1];9if (best[e[0]] == NONE || (longest ? cand > best[e[0]] : cand < best[e[0]])) best[e[0]] = cand;10}11}12return best;13}14long[] countPaths(int n, List<int[]>[] adj, int[] order, int s) {15long[] ways = new long[n]; ways[s] = 1;16for (int u : order) for (int[] e : adj[u]) ways[e[0]] += ways[u]; // paths into u extend along every arrow out of u17return ways;18}
Live state
order
[0, 1, 2, 3, 5, 4]
source
0
Execution trace (dry run table)Show 11 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | order=[0, 1, 2, 3, 5, 4] source=0 | A DAG has a topological order: 0 → 1 → 2 → 3 → 5 → 4. Processing vertices in that order means every arrow into a vertex has been handled before the vertex is used, so its value is final. The source starts at 0. |
| 2 | L9 | u=0 v=1 cand=5 best[v]=5 | 0 → 1: 0 + 5 = 5. That beats the old value, so best[1] = 5. |
| 3 | L9 | u=0 v=2 cand=3 best[v]=3 | 0 → 2: 0 + 3 = 3. That beats the old value, so best[2] = 3. |
| 4 | L9 | u=1 v=3 cand=11 best[v]=11 | 1 → 3: 5 + 6 = 11. That beats the old value, so best[3] = 11. |
| 5 | L9 | u=1 v=2 cand=7 best[v]=7 | 1 → 2: 5 + 2 = 7. That beats the old value, so best[2] = 7. |
| 6 | L9 | u=2 v=4 cand=11 best[v]=11 | 2 → 4: 7 + 4 = 11. That beats the old value, so best[4] = 11. |
| 7 | L9 | u=2 v=5 cand=9 best[v]=9 | 2 → 5: 7 + 2 = 9. That beats the old value, so best[5] = 9. |
| 8 | L9 | u=2 v=3 cand=14 best[v]=14 | 2 → 3: 7 + 7 = 14. That beats the old value, so best[3] = 14. |
| 9 | L9 | u=3 v=5 cand=15 best[v]=15 | 3 → 5: 14 + 1 = 15. That beats the old value, so best[5] = 15. |
| 10 | L9 | u=3 v=4 cand=15 best[v]=15 | 3 → 4: 14 + 1 = 15. That beats the old value, so best[4] = 15. |
| 11 | L11 | return=[0, 5, 7, 14, 15, 15] arrows relaxed=9 | Values: [0, 5, 7, 14, 15, 15] ('-' = not reachable). Every arrow was used exactly once, so the whole computation is O(n + m). The longest path ends at 4 with length 15: 0 → 1 → 2 → 3 → 4 (read back through the parent links). |
Count paths
To count the paths from s to every vertex, let ways[s] = 1 and, in topological order, add ways[u] to ways[v] for each arrow u → v. Every path to v ends with exactly one arrow u → v and a path to u, so the sum is the total. Parallel arrows count separately.
Counts grow quickly (a chain of diamonds doubles at each step), so use long or work modulo a prime when the answer would overflow. The animation's *Count the paths* option shows the counts building up.
1static long[] countPaths(int n, int[][] edges, int s) { // edges {u, v}; ways[v] = number of paths s -> v2int[] indeg = new int[n], order = new int[n];3for (int[] e : edges) indeg[e[1]]++;4Deque<Integer> q = new ArrayDeque<>();5for (int v = 0; v < n; v++) if (indeg[v] == 0) q.add(v);6int k = 0;7while (!q.isEmpty()) {8int u = q.poll();9order[k++] = u;10for (int[] e : edges) if (e[0] == u && --indeg[e[1]] == 0) q.add(e[1]);11}12long[] ways = new long[n];13ways[s] = 1; // the empty path14for (int u : order)15for (int[] e : edges)16if (e[0] == u) ways[e[1]] += ways[u]; // every path to u extends along this arrow17return ways;18}
Dynamic programming over a topological order: longest, shortest, count of paths
Interactive · edit the inputdirected graph · 6 vertices · 9 edges
d=0
topological order
best[v]
A DAG has a topological order: 0 → 1 → 2 → 3 → 5 → 4. Processing vertices in that order means every arrow into a vertex has been handled before the vertex is used, so its value is final. The source starts at 0.
1long[] dagBest(int n, List<int[]>[] adj, int[] order, int s, boolean longest) { // adj[u] = {v, weight}2long NONE = longest ? Long.MIN_VALUE : Long.MAX_VALUE;3long[] best = new long[n]; Arrays.fill(best, NONE);4best[s] = 0;5for (int u : order) { // topological order: best[u] is final when u is reached6if (best[u] == NONE) continue; // not reachable from s7for (int[] e : adj[u]) {8long cand = best[u] + e[1];9if (best[e[0]] == NONE || (longest ? cand > best[e[0]] : cand < best[e[0]])) best[e[0]] = cand;10}11}12return best;13}14long[] countPaths(int n, List<int[]>[] adj, int[] order, int s) {15long[] ways = new long[n]; ways[s] = 1;16for (int u : order) for (int[] e : adj[u]) ways[e[0]] += ways[u]; // paths into u extend along every arrow out of u17return ways;18}
Live state
order
[0, 1, 2, 3, 5, 4]
source
0
Execution trace (dry run table)Show 11 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | order=[0, 1, 2, 3, 5, 4] source=0 | A DAG has a topological order: 0 → 1 → 2 → 3 → 5 → 4. Processing vertices in that order means every arrow into a vertex has been handled before the vertex is used, so its value is final. The source starts at 0. |
| 2 | L9 | u=0 v=1 cand=5 best[v]=5 | 0 → 1: 0 + 5 = 5. That beats the old value, so best[1] = 5. |
| 3 | L9 | u=0 v=2 cand=3 best[v]=3 | 0 → 2: 0 + 3 = 3. That beats the old value, so best[2] = 3. |
| 4 | L9 | u=1 v=3 cand=11 best[v]=11 | 1 → 3: 5 + 6 = 11. That beats the old value, so best[3] = 11. |
| 5 | L9 | u=1 v=2 cand=7 best[v]=7 | 1 → 2: 5 + 2 = 7. That beats the old value, so best[2] = 7. |
| 6 | L9 | u=2 v=4 cand=11 best[v]=11 | 2 → 4: 7 + 4 = 11. That beats the old value, so best[4] = 11. |
| 7 | L9 | u=2 v=5 cand=9 best[v]=9 | 2 → 5: 7 + 2 = 9. That beats the old value, so best[5] = 9. |
| 8 | L9 | u=2 v=3 cand=14 best[v]=14 | 2 → 3: 7 + 7 = 14. That beats the old value, so best[3] = 14. |
| 9 | L9 | u=3 v=5 cand=15 best[v]=15 | 3 → 5: 14 + 1 = 15. That beats the old value, so best[5] = 15. |
| 10 | L9 | u=3 v=4 cand=15 best[v]=15 | 3 → 4: 14 + 1 = 15. That beats the old value, so best[4] = 15. |
| 11 | L11 | return=[0, 5, 7, 14, 15, 15] arrows relaxed=9 | Values: [0, 5, 7, 14, 15, 15] ('-' = not reachable). Every arrow was used exactly once, so the whole computation is O(n + m). The longest path ends at 4 with length 15: 0 → 1 → 2 → 3 → 4 (read back through the parent links). |
DP over topological order
All three problems share a pattern: *the answer for a vertex depends only on its predecessors*. That is exactly a dynamic program whose states are the vertices and whose evaluation order is a topological order. Memoised recursion over the DAG does the same work in a different order.
Other examples are the longest chain in a partial order (the number of edges on the longest path), the number of ways to build something from prerequisites, and many game-state problems. The code below finds the longest path in edges, starting anywhere.
1static int longestChain(int n, int[][] edges) { // most arrows on any path; -1 if the graph has a cycle2int[] indeg = new int[n], len = new int[n]; // len[v] = most arrows on a path ending at v3for (int[] e : edges) indeg[e[1]]++;4Deque<Integer> q = new ArrayDeque<>();5for (int v = 0; v < n; v++) if (indeg[v] == 0) q.add(v);6int seen = 0, best = 0;7while (!q.isEmpty()) {8int u = q.poll();9seen++;10best = Math.max(best, len[u]);11for (int[] e : edges)12if (e[0] == u) {13len[e[1]] = Math.max(len[e[1]], len[u] + 1);14if (--indeg[e[1]] == 0) q.add(e[1]);15}16}17return seen == n ? best : -1;18}
Dynamic programming over a topological order: longest, shortest, count of paths
Interactive · edit the inputdirected graph · 6 vertices · 9 edges
d=0
topological order
best[v]
A DAG has a topological order: 0 → 1 → 2 → 3 → 5 → 4. Processing vertices in that order means every arrow into a vertex has been handled before the vertex is used, so its value is final. The source starts at 0.
1long[] dagBest(int n, List<int[]>[] adj, int[] order, int s, boolean longest) { // adj[u] = {v, weight}2long NONE = longest ? Long.MIN_VALUE : Long.MAX_VALUE;3long[] best = new long[n]; Arrays.fill(best, NONE);4best[s] = 0;5for (int u : order) { // topological order: best[u] is final when u is reached6if (best[u] == NONE) continue; // not reachable from s7for (int[] e : adj[u]) {8long cand = best[u] + e[1];9if (best[e[0]] == NONE || (longest ? cand > best[e[0]] : cand < best[e[0]])) best[e[0]] = cand;10}11}12return best;13}14long[] countPaths(int n, List<int[]>[] adj, int[] order, int s) {15long[] ways = new long[n]; ways[s] = 1;16for (int u : order) for (int[] e : adj[u]) ways[e[0]] += ways[u]; // paths into u extend along every arrow out of u17return ways;18}
Live state
order
[0, 1, 2, 3, 5, 4]
source
0
Execution trace (dry run table)Show 11 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | order=[0, 1, 2, 3, 5, 4] source=0 | A DAG has a topological order: 0 → 1 → 2 → 3 → 5 → 4. Processing vertices in that order means every arrow into a vertex has been handled before the vertex is used, so its value is final. The source starts at 0. |
| 2 | L9 | u=0 v=1 cand=5 best[v]=5 | 0 → 1: 0 + 5 = 5. That beats the old value, so best[1] = 5. |
| 3 | L9 | u=0 v=2 cand=3 best[v]=3 | 0 → 2: 0 + 3 = 3. That beats the old value, so best[2] = 3. |
| 4 | L9 | u=1 v=3 cand=11 best[v]=11 | 1 → 3: 5 + 6 = 11. That beats the old value, so best[3] = 11. |
| 5 | L9 | u=1 v=2 cand=7 best[v]=7 | 1 → 2: 5 + 2 = 7. That beats the old value, so best[2] = 7. |
| 6 | L9 | u=2 v=4 cand=11 best[v]=11 | 2 → 4: 7 + 4 = 11. That beats the old value, so best[4] = 11. |
| 7 | L9 | u=2 v=5 cand=9 best[v]=9 | 2 → 5: 7 + 2 = 9. That beats the old value, so best[5] = 9. |
| 8 | L9 | u=2 v=3 cand=14 best[v]=14 | 2 → 3: 7 + 7 = 14. That beats the old value, so best[3] = 14. |
| 9 | L9 | u=3 v=5 cand=15 best[v]=15 | 3 → 5: 14 + 1 = 15. That beats the old value, so best[5] = 15. |
| 10 | L9 | u=3 v=4 cand=15 best[v]=15 | 3 → 4: 14 + 1 = 15. That beats the old value, so best[4] = 15. |
| 11 | L11 | return=[0, 5, 7, 14, 15, 15] arrows relaxed=9 | Values: [0, 5, 7, 14, 15, 15] ('-' = not reachable). Every arrow was used exactly once, so the whole computation is O(n + m). The longest path ends at 4 with length 15: 0 → 1 → 2 → 3 → 4 (read back through the parent links). |
Practice ladder
Now try it yourself
Easy
- All Paths From Source to Target
Medium
- Parallel Courses III
- Longest Increasing Path in a Matrix
- Find All Possible Recipes from Given Supplies
Advanced
- Largest Color Value in a Directed Graph
- Longest Cycle in a Graph
Key takeaways
- On a DAG, process vertices in topological order and relax each arrow once: O(n + m).
- Negative weights are fine on a DAG, and no priority queue is needed.
- Path counts add the count of each predecessor; use long or a modulus.
- Any quantity that depends only on predecessors is a DP over a topological order.