Longest paths
The longest path is easy on a DAG and hard in general graphs; the DAG algorithm also reconstructs the path from parent links.
Learning goals
- Compute the longest path between two vertices of a DAG
- Explain why the longest simple path is hard on graphs with cycles
- Rebuild the path itself
Prerequisites
The longest chain of dependent tasks decides how long a project takes. On a DAG that is a one-pass computation; remove the no-cycle guarantee and the same question becomes one of the classic hard problems.
Longest path in DAG
Process vertices in topological order and relax each arrow with max instead of min: longest[v] = max(longest[v], longest[u] + w). A vertex's value is final when it is reached, because all its predecessors came earlier. Cost O(n + m). Vertices not reachable from the source keep "none".
Weights may be negative here too. The first option of the DAG animation, *Longest path from the source*, prints the longest path found and its length.
1static long longestPathDag(int n, int[][] edges, int s, int t) { // Long.MIN_VALUE when t is unreachable from s2int[] order = topoOrder(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[t];12}1314static int[] topoOrder(int n, int[][] edges) { // Kahn's order of a DAG15int[] indeg = new int[n], order = new int[n];16for (int[] e : edges) indeg[e[1]]++;17Deque<Integer> q = new ArrayDeque<>();18for (int v = 0; v < n; v++) if (indeg[v] == 0) q.add(v);19int k = 0;20while (!q.isEmpty()) {21int u = q.poll();22order[k++] = u;23for (int[] e : edges) if (e[0] == u && --indeg[e[1]] == 0) q.add(e[1]);24}25return order;26}
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). |
Why general longest simple path is hard
In a graph with cycles a walk can go round a positive cycle forever, so the longest *walk* is unbounded; the longest **simple path** (no repeated vertex) is the sensible question, and it is NP-hard: no polynomial algorithm is known, and one would imply P = NP. The shortest-path tricks fail because subpaths of a longest simple path need not be longest, and the visited set matters.
The exhaustive search below tries every simple path. On the complete directed graph with n vertices, the number of calls grows factorially: the test run counted 130 calls for n = 6, 652 for 7, 3,914 for 8, 27,400 for 9 and 219,202 for 10, the ratio between steps grows from 5 to 8. Special cases are easy (DAGs, trees, and some structured graphs), and dynamic programming over subsets reaches O(2^n · n²).
1static class LongestSimple {2long calls = 0; // how many times search ran3private long best;4long longest(int n, int[][] edges, int s, int t) { // exhaustive over simple paths; Long.MIN_VALUE if none5best = Long.MIN_VALUE;6calls = 0;7search(edges, s, t, 0, new boolean[n]);8return best;9}10private void search(int[][] edges, int u, int t, long cost, boolean[] used) {11calls++;12if (u == t) { best = Math.max(best, cost); return; }13used[u] = true;14for (int[] e : edges) if (e[0] == u && !used[e[1]]) search(edges, e[1], t, cost + e[2], used);15used[u] = false;16}17}
Reconstructing path
Store with each vertex the predecessor that gave its best value: parent[v] = u whenever longest[u] + w improves longest[v]. After the pass, follow parents from the target back to the source and reverse. If the target was never reached, the path is empty.
The same trick rebuilds shortest paths and critical paths of a project. Ties may give several optimal paths; the parent links record whichever was found first.
1static List<Integer> longestPathVertices(int n, int[][] edges, int s, int t) { // vertices of a longest path s -> t, or empty2int[] order = topoOrder(n, edges);3long[] best = new long[n];4int[] parent = new int[n];5Arrays.fill(best, Long.MIN_VALUE);6Arrays.fill(parent, -1);7best[s] = 0;8for (int u : order) {9if (best[u] == Long.MIN_VALUE) continue;10for (int[] e : edges)11if (e[0] == u && best[u] + e[2] > best[e[1]]) { best[e[1]] = best[u] + e[2]; parent[e[1]] = u; }12}13LinkedList<Integer> path = new LinkedList<>();14if (best[t] == Long.MIN_VALUE) return path;15for (int v = t; v != -1; v = parent[v]) path.addFirst(v);16return path;17}1819static int[] topoOrder(int n, int[][] edges) { // Kahn's order of a DAG20int[] indeg = new int[n], order = new int[n];21for (int[] e : edges) indeg[e[1]]++;22Deque<Integer> q = new ArrayDeque<>();23for (int v = 0; v < n; v++) if (indeg[v] == 0) q.add(v);24int k = 0;25while (!q.isEmpty()) {26int u = q.poll();27order[k++] = u;28for (int[] e : edges) if (e[0] == u && --indeg[e[1]] == 0) q.add(e[1]);29}30return order;31}
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
Advanced
- Longest Cycle in a Graph
- Largest Color Value in a Directed Graph
Key takeaways
- Longest path on a DAG: relax with max in topological order, O(n + m); store parents to rebuild it.
- With cycles the longest simple path is NP-hard; brute force explores factorially many paths.
- Negative weights are fine on a DAG.