196. Longest Path in a DAG
Problem
For a weighted DAG with n vertices and edges (u,v,weight), return the maximum total weight of a path that may start and finish anywhere. Weights may be negative. A zero-edge path has weight zero, so the answer is at least zero, including n=0. Raise ValueError if the graph is cyclic.
Examples
Example 1
longest_dag_path(3,[(0,1,-5),(1,2,7)])
=> 7A path may begin after an unfavorable prefix.
Example 2
longest_dag_path(4,[(0,1,1),(1,2,1),(0,3,1)])
=> 2Unit weights count edges on the longest route.
Example 3
longest_dag_path(0,[])
=> 0The empty graph uses the defined zero result.
Reveal the worked solution and reasoning
Why this mechanism works
Topological order ensures every predecessor contributes before a vertex is used to extend a path. Store the best weight of a path ending at each vertex. Initializing every value to zero allows a path to start anywhere and prevents a negative prefix from being forced into a later path. Unit weights specialize this to unweighted path length in edges.
State and boundaries
best[v] is the largest known weight ending at v. When v is processed in topological order, all incoming alternatives have already been considered.
Protocol
- Build weighted and unweighted adjacency.
- Obtain a full topological order or reject a cycle.
- Initialize all best values to zero.
- For each ordered u, relax every edge with best[u]+weight.
- Return the maximum, defaulting to zero.
Visualization
0 --(-5)--> 1 --(7)--> 2
best initially [0,0,0]
best[1] remains 0; best[2] becomes 7
optimal path starts at 1, avoiding the negative prefixPython implementation
from kata_193 import topological_order
def longest_dag_path(n, edges):
adj, weighted = [[] for _ in range(n)], [[] for _ in range(n)]
for u, v, weight in edges:
adj[u].append(v)
weighted[u].append((v, weight))
order = topological_order(adj)
if len(order) != n:
raise ValueError('graph must be acyclic')
best = [0] * n
for u in order:
for v, weight in weighted[u]:
best[v] = max(best[v], best[u] + weight)
return max(best, default=0)
Worked execution
longest_dag_path(3,[(0,1,-5),(1,2,7)])
L4 longest_dag_path: adj, weighted = [[] for _ in range(n)], [[] for _ in range(n)] n=3; edges=[(0, 1, -5), (1, 2, 7)]; _=0 L4 longest_dag_path: adj, weighted = [[] for _ in range(n)], [[] for _ in range(n)] n=3; edges=[(0, 1, -5), (1, 2, 7)]; _=1 L4 longest_dag_path: adj, weighted = [[] for _ in range(n)], [[] for _ in range(n)] n=3; edges=[(0, 1, -5), (1, 2, 7)]; _=2 L4 longest_dag_path: adj, weighted = [[] for _ in range(n)], [[] for _ in range(n)] n=3; edges=[(0, 1, -5), (1, 2, 7)]; _=0 L4 longest_dag_path: adj, weighted = [[] for _ in range(n)], [[] for _ in range(n)] n=3; edges=[(0, 1, -5), (1, 2, 7)]; _=1 L4 longest_dag_path: adj, weighted = [[] for _ in range(n)], [[] for _ in range(n)] n=3; edges=[(0, 1, -5), (1, 2, 7)]; _=2 L4 longest_dag_path: adj, weighted = [[] for _ in range(n)], [[] for _ in range(n)] n=3; edges=[(0, 1, -5), (1, 2, 7)]; adj=[[], [], []]; weighted=[[], [], []] L5 longest_dag_path: for u, v, weight in edges: n=3; edges=[(0, 1, -5), (1, 2, 7)]; adj=[[], [], []]; weighted=[[], [], []]; u=0; v=1; weight=-5 ... 9 intermediate state changes omitted ... L12 longest_dag_path: for u in order: n=3; edges=[(0, 1, -5), (1, 2, 7)]; adj=[[1], [2], []]; weighted=[[(..., ...)], [(..., ...)], []]; u=1; v=1; weight=-5; order=[0, 1, 2]; best=[0, 0, 0] L13 longest_dag_path: for v, weight in weighted[u]: n=3; edges=[(0, 1, -5), (1, 2, 7)]; adj=[[1], [2], []]; weighted=[[(..., ...)], [(..., ...)], []]; u=1; v=2; weight=7; order=[0, 1, 2]; best=[0, 0, 0] L14 longest_dag_path: best[v] = max(best[v], best[u] + weight) n=3; edges=[(0, 1, -5), (1, 2, 7)]; adj=[[1], [2], []]; weighted=[[(..., ...)], [(..., ...)], []]; u=1; v=2; weight=7; order=[0, 1, 2]; best=[0, 0, 7] L12 longest_dag_path: for u in order: n=3; edges=[(0, 1, -5), (1, 2, 7)]; adj=[[1], [2], []]; weighted=[[(..., ...)], [(..., ...)], []]; u=2; v=2; weight=7; order=[0, 1, 2]; best=[0, 0, 7]
Result: 7. A path may begin after an unfavorable prefix.
longest_dag_path(4,[(0,1,1),(1,2,1),(0,3,1)])
L4 longest_dag_path: adj, weighted = [[] for _ in range(n)], [[] for _ in range(n)] n=4; edges=[(0, 1, 1), (1, 2, 1), (0, 3, 1)]; _=0 L4 longest_dag_path: adj, weighted = [[] for _ in range(n)], [[] for _ in range(n)] n=4; edges=[(0, 1, 1), (1, 2, 1), (0, 3, 1)]; _=1 L4 longest_dag_path: adj, weighted = [[] for _ in range(n)], [[] for _ in range(n)] n=4; edges=[(0, 1, 1), (1, 2, 1), (0, 3, 1)]; _=2 L4 longest_dag_path: adj, weighted = [[] for _ in range(n)], [[] for _ in range(n)] n=4; edges=[(0, 1, 1), (1, 2, 1), (0, 3, 1)]; _=3 L4 longest_dag_path: adj, weighted = [[] for _ in range(n)], [[] for _ in range(n)] n=4; edges=[(0, 1, 1), (1, 2, 1), (0, 3, 1)]; _=0 L4 longest_dag_path: adj, weighted = [[] for _ in range(n)], [[] for _ in range(n)] n=4; edges=[(0, 1, 1), (1, 2, 1), (0, 3, 1)]; _=1 L4 longest_dag_path: adj, weighted = [[] for _ in range(n)], [[] for _ in range(n)] n=4; edges=[(0, 1, 1), (1, 2, 1), (0, 3, 1)]; _=2 L4 longest_dag_path: adj, weighted = [[] for _ in range(n)], [[] for _ in range(n)] n=4; edges=[(0, 1, 1), (1, 2, 1), (0, 3, 1)]; _=3 ... 17 intermediate state changes omitted ... L13 longest_dag_path: for v, weight in weighted[u]: n=4; edges=[(0, 1, 1), (1, 2, 1), (0, 3, 1)]; adj=[[1, 3], [2], [], []]; weighted=[[(..., ...), (..., ...)], [(..., ...)], [], []]; u=1; v=2; weight=1; order=[0, 1, 3, 2]; best=[0, 1, 0, 1] L14 longest_dag_path: best[v] = max(best[v], best[u] + weight) n=4; edges=[(0, 1, 1), (1, 2, 1), (0, 3, 1)]; adj=[[1, 3], [2], [], []]; weighted=[[(..., ...), (..., ...)], [(..., ...)], [], []]; u=1; v=2; weight=1; order=[0, 1, 3, 2]; best=[0, 1, 2, 1] L12 longest_dag_path: for u in order: n=4; edges=[(0, 1, 1), (1, 2, 1), (0, 3, 1)]; adj=[[1, 3], [2], [], []]; weighted=[[(..., ...), (..., ...)], [(..., ...)], [], []]; u=3; v=2; weight=1; order=[0, 1, 3, 2]; best=[0, 1, 2, 1] L12 longest_dag_path: for u in order: n=4; edges=[(0, 1, 1), (1, 2, 1), (0, 3, 1)]; adj=[[1, 3], [2], [], []]; weighted=[[(..., ...), (..., ...)], [(..., ...)], [], []]; u=2; v=2; weight=1; order=[0, 1, 3, 2]; best=[0, 1, 2, 1]
Result: 2. Unit weights count edges on the longest route.
Why the solution is correct
Any nonempty path ending at v extends a path ending at a predecessor u. The recurrence considers every such last edge after best[u] is final; zero also considers starting at v. Induction along topological order makes each result exact, and their maximum covers every endpoint.
Time and space
O(V+E) time and space. Negative edges are safe because acyclicity, rather than nonnegative weights, orders dependencies.
Common mistakes
Using a single-source initialization answers a different question. Processing vertices by numeric ID is not necessarily a topological order.
Reasoning card
State: best[v] is the largest known weight ending at v. When v is processed in topological order, all incoming alternatives have already been considered.
Invariant: All predecessors are finalized before a node is processed.
Decision to rehearse: Relax edges in topological order.
Finish: Return the maximum, defaulting to zero
Transfer exercise
Require a nonempty path, then revise the initialization and all-negative case explicitly.