⚙️AlgorithmIntermediate

Topological Sort - DP on DAG

Key Points

  • Topological sort orders vertices of a directed acyclic graph (DAG) so every edge goes from earlier to later, which is perfect for dynamic programming (DP).
  • Processing nodes in topological order guarantees that when you compute dp[v], all required predecessor states are already computed.
  • Forward DP computes values that flow along edge direction, like longest distance from a source or number of paths.
  • Backward DP computes values that flow against edge direction, like shortest distance to a sink.
  • The longest path in a DAG is solvable in O(V + E) time by DP if the graph is acyclic, even with negative weights.
  • Counting paths from a source to all vertices is simply a sum over predecessors in topological order and can be done modulo a large prime.
  • Shortest path to a fixed sink can be computed by processing nodes in reverse topological order using a min transition.
  • Many scheduling, prerequisite, and pipeline problems reduce to DP on DAGs after modeling states as vertices and transitions as edges.

Prerequisites

  • Graph fundamentalsUnderstanding vertices, directed edges, adjacency lists, and indegree/outdegree is essential to build and traverse DAGs.
  • Breadth-first and depth-first searchFamiliarity with BFS/DFS helps in implementing or understanding topological sorting and graph traversal.
  • Topological sortingKnowing Kahn’s algorithm or DFS postorder is required to obtain a valid processing order for DP on DAGs.
  • Dynamic programming basicsYou must be comfortable defining states, base cases, and transitions to model problems as DP on graphs.
  • Big-O notation and complexityTo analyze why DAG DP runs in O(V + E) and how memory usage scales.
  • Modular arithmeticNeeded when counting paths where the number of paths can be very large and results must be taken modulo a prime.

Detailed Explanation

Tap terms for definitions

01Overview

Imagine you have a list of tasks, each with prerequisites. You can only start a task after all its prerequisites are done. If you line up tasks so that every prerequisite comes before the task, you get a topological order. Dynamic programming (DP) on a directed acyclic graph (DAG) uses this order to compute answers that depend on earlier results. The big win is that acyclicity removes circular dependencies, so we can make one pass that always has the needed sub-results ready. In practice, we replace a complicated process by a graph where vertices are states (or tasks) and edges represent valid transitions. Then, by visiting vertices in topological order, we compute dp values using local transitions like max, min, or sum. For example, the longest path in a DAG (critical path) can be found by maximizing dp over incoming edges; counting paths from a source sums over incoming paths; and the shortest path to a sink minimizes over outgoing edges if we sweep backward. This technique turns many problems into a clean O(V + E) solution: build the DAG, find a topological order, define base cases, and relax edges in the correct order.

02Intuition & Analogies

Think of building a Lego model following instructions. Each step requires certain pieces already assembled; you wouldn’t attempt step 10 before finishing steps 7 and 8. A topological order is that instruction list: prerequisites appear earlier than dependents. Now imagine you’re also tracking the tallest height of the model at each step. When you reach step v, the tallest it can be is the tallest of any previous step u that connects to v, plus the contribution of this step. That’s exactly forward DP: dp[v] = combine over predecessors u. Alternatively, picture draining water from a network of pipes that only flow downhill (no cycles). If you want to know how long it takes water at some junction to reach the final drain (sink), you’d look ahead to all downstream junctions and take the minimum time; that’s backward DP: dp[v] = min over successors. The absence of cycles ensures you will never be stuck needing a value that depends on itself. The topological order is your guaranteed schedule: when you compute dp[v], every required dp[u] is already set. This is why problems like course scheduling, project scheduling (critical path), path counting in grid-like DAGs, or state-machine transitions all become straightforward once modeled as a DAG and processed in topological order.

03Formal Definition

Let G = (V, E) be a directed acyclic graph (DAG). A topological ordering is a bijection ord: V → {1, 2, …, } such that for every edge (u, v) ∈ E, ord(u) < ord(v). Dynamic programming on a DAG defines a state function dp: over some semiring or algebraic structure (e.g., reals with max/+ or min/+; integers with +/* modulo M), together with base cases on a subset B ⊆ V. A forward DP specifies transitions of the form dp[v] = ⊕_{(u, v) ∈ E} f(dp[u], w(u, v)), where ⊕ is an associative operator like max or sum, and w(u, v) is an edge weight. A backward DP specifies dp[v] = ⊕_{(v, u) ∈ E} f(dp[u], w(v, u)), often computed by traversing the topological order in reverse. Because G is acyclic, the partial order induced by reachability guarantees that if ord respects edges, then for every edge (u, v), dp[u] is computed before dp[v] in forward DP (and dp[u] after dp[v] in backward DP). This yields linear-time evaluation in the graph size, assuming f and ⊕ are O(1), after a topological order is obtained. Classical instances include the longest path in DAG (⊕ = max, f(a, w) = a + w), number of paths (⊕ = sum, f(a, 1) = a), and shortest path to a sink (⊕ = min, f(a, w) = w + a).

04When to Use

Use DP on DAG whenever you can model your problem as states connected by one-way dependencies with no cycles. Typical cases include: scheduling with prerequisites (courses, tasks, builds), where you want longest completion time (critical path) or the minimum number of stages; path problems where the graph is known to be acyclic (like layered graphs, grids with monotone moves, or time-expanded networks), to compute shortest/longest distances or count paths; dynamic programs where transitions always go to a strictly later stage (e.g., DP indexed by time, position, or length), which naturally forms a DAG by design; and problems where compressing strongly connected components (SCCs) yields a condensation DAG, on which you can run DP over components. It’s especially attractive when edge relaxations are simple (O(1)) and you need all-vertices answers, since the total cost becomes O(V + E). If you face negative edge weights but no cycles, DAG DP still works for min or max because acyclicity prevents infinite descent/ascent. Reach for this technique before more complex algorithms (like Dijkstra or Bellman–Ford) whenever you can assert acyclicity or construct a layered DAG model.

⚠️Common Mistakes

Common pitfalls include: forgetting to verify the graph is acyclic—if the queue-based topological sort processes fewer than V nodes, you must detect and halt; using the wrong initial values—maximization DPs need −∞ for unreachable states, minimization need +∞, and counts need 0; mixing up forward and backward transitions—ensure your recurrence matches the direction you traverse the topological order; not setting correct base cases—e.g., dp[source] = 0 (longest/shortest path) or 1 (path counts), and dp[sink] = 0 for backward shortest paths; integer overflow—use 64-bit integers for distances and modulo arithmetic for large path counts; mishandling multiple sources—either add a super-source with 0-weight edges or initialize all true sources consistently; ignoring unreachable vertices—keep them at −∞/+∞/0 as appropriate and avoid relaxing through them; and recursion depth limits when using DFS-based topological sort—prefer Kahn’s algorithm or increase stack limits. Finally, ensure your adjacency lists and indegrees are built with correct indexing and no stray self-loops that could violate assumptions if your modeling forbids them.

Key Formulas

Topological Ordering Property

Explanation: A topological order places every edge from an earlier vertex to a later vertex. This linearizes the partial order and guarantees DP dependencies are satisfied.

Longest Path in DAG (Forward DP)

Explanation: The longest distance to v is the best among all predecessors u plus the edge weight. Initialize the source to 0 and others to negative infinity to indicate unreachable states.

Counting Paths (Forward DP)

Explanation: The number of paths to v is the sum of the number of paths to each predecessor. Start with one path at the source and zero elsewhere; often computed modulo a large prime.

Shortest Path to Sink (Backward DP)

Explanation: The shortest distance from v to the sink t equals the minimum over all immediate successors u of the edge weight plus the best distance from u to t. Initialize the sink to 0 and others to infinity.

Edge Accounting

Explanation: When processing each edge once during DP transitions, the total number of relaxations is exactly the number of edges. This underpins the O(V + E) runtime.

Time Complexity on DAG

Explanation: Computing a topological order and relaxing each edge once leads to linear time in the size of the graph. This assumes O(1) per edge relaxation.

Complexity Analysis

Dynamic programming on DAGs generally runs in linear time with respect to the graph size. Computing a topological order via Kahn’s algorithm processes each vertex once and each edge once when decrementing indegrees, taking O( + ). After we have the order, a single DP sweep performs one relaxation per edge, because each transition is evaluated exactly once: the total number of transition evaluations is outdeg(v) = . Therefore, the total time complexity is O( + ) for standard problems like longest path, shortest path to a sink, and path counting. Space complexity is typically O( + ) to store the adjacency list and O() to store dp arrays, indegrees, and the topological order, yielding O( + ) overall memory. For variants with multiple sources or sinks, adding a super-source/super-sink does not change asymptotic bounds. Handling modulo arithmetic for path counts adds only constant-time operations per edge. Compared to general-graph algorithms, DAG DP avoids priority queues (Dijkstra) or repeated passes (Bellman–Ford), which is why it’s both simpler and faster when acyclicity holds. If you attempt to run these DP procedures on a graph with cycles, either the topological sort will fail (queue empties before processing all vertices) or the recurrence may create circular dependencies; detecting non-DAG input is essential to preserve correctness and complexity guarantees.

Code Examples

Topological Sort + Longest Path in a Weighted DAG (Forward DP)
1#include <bits/stdc++.h>
2using namespace std;
3
4// Compute topological order with Kahn's algorithm and then the longest path from a given source.
5// Works even with negative edge weights because the graph is acyclic.
6
7int main() {
8 ios::sync_with_stdio(false);
9 cin.tie(nullptr);
10
11 int n, m; // number of vertices and edges
12 // Example input:
13 // 6 7
14 // 1 2 2
15 // 1 3 1
16 // 2 4 3
17 // 2 5 4
18 // 3 5 2
19 // 4 6 1
20 // 5 6 3
21 // 1
22 if (!(cin >> n >> m)) {
23 cerr << "Expected n m on input\n";
24 return 0;
25 }
26
27 struct Edge { int to; long long w; };
28 vector<vector<Edge>> g(n + 1);
29 vector<int> indeg(n + 1, 0);
30 for (int i = 0; i < m; ++i) {
31 int u, v; long long w; cin >> u >> v >> w;
32 g[u].push_back({v, w});
33 indeg[v]++;
34 }
35
36 int s; cin >> s; // source vertex
37
38 // Kahn's algorithm for topological order
39 queue<int> q;
40 for (int v = 1; v <= n; ++v) if (indeg[v] == 0) q.push(v);
41
42 vector<int> topo; topo.reserve(n);
43 while (!q.empty()) {
44 int u = q.front(); q.pop();
45 topo.push_back(u);
46 for (auto &e : g[u]) {
47 if (--indeg[e.to] == 0) q.push(e.to);
48 }
49 }
50
51 if ((int)topo.size() != n) {
52 cout << "The graph is not a DAG (cycle detected)." << '\n';
53 return 0;
54 }
55
56 const long long NEG_INF = (long long)-4e18; // sentinel for unreachable
57 vector<long long> dist(n + 1, NEG_INF);
58 dist[s] = 0;
59
60 // Forward DP in topological order: relax edges from each u
61 for (int u : topo) {
62 if (dist[u] == NEG_INF) continue; // unreachable; skip
63 for (auto &e : g[u]) {
64 dist[e.to] = max(dist[e.to], dist[u] + e.w);
65 }
66 }
67
68 // Output longest distances from source to every vertex
69 // Unreachable vertices remain as "-INF"
70 for (int v = 1; v <= n; ++v) {
71 if (dist[v] == NEG_INF) cout << "-INF\n";
72 else cout << dist[v] << '\n';
73 }
74
75 return 0;
76}
77

This program reads a weighted DAG, computes a topological order using Kahn’s algorithm, and then performs a single forward DP pass to compute the longest distance from a given source s to every vertex. Unreachable vertices are marked with negative infinity. Acyclicity guarantees that when we process a vertex u, all dp[u] values are already finalized.

Time: O(n + m)Space: O(n + m)
Counting Number of Paths from s to t in a DAG (Modulo 1e9+7)
1#include <bits/stdc++.h>
2using namespace std;
3
4// Count the number of distinct directed paths from s to t in a DAG using DP in topological order.
5// Computation is done modulo MOD.
6
7int main() {
8 ios::sync_with_stdio(false);
9 cin.tie(nullptr);
10
11 const int MOD = 1'000'000'007;
12
13 int n, m; // vertices and edges
14 // Example input:
15 // 5 6
16 // 1 2
17 // 1 3
18 // 2 4
19 // 3 4
20 // 2 5
21 // 4 5
22 // 1 5
23 if (!(cin >> n >> m)) return 0;
24
25 vector<vector<int>> g(n + 1);
26 vector<int> indeg(n + 1, 0);
27 for (int i = 0; i < m; ++i) {
28 int u, v; cin >> u >> v;
29 g[u].push_back(v);
30 indeg[v]++;
31 }
32 int s, t; cin >> s >> t;
33
34 // Kahn's algorithm for topological order
35 queue<int> q;
36 for (int v = 1; v <= n; ++v) if (indeg[v] == 0) q.push(v);
37
38 vector<int> topo; topo.reserve(n);
39 while (!q.empty()) {
40 int u = q.front(); q.pop();
41 topo.push_back(u);
42 for (int v : g[u]) if (--indeg[v] == 0) q.push(v);
43 }
44
45 if ((int)topo.size() != n) {
46 cout << 0 << '\n'; // cycles imply infinite paths; for DAG DP, we treat as zero/invalid
47 return 0;
48 }
49
50 vector<int> ways(n + 1, 0);
51 ways[s] = 1;
52
53 // DP: propagate path counts along edges
54 for (int u : topo) {
55 if (ways[u] == 0) continue;
56 for (int v : g[u]) {
57 ways[v] += ways[u];
58 if (ways[v] >= MOD) ways[v] -= MOD;
59 }
60 }
61
62 cout << ways[t] % MOD << '\n';
63 return 0;
64}
65

After computing a topological order, the program initializes ways[s] = 1 and then accumulates path counts along outgoing edges. By the time a vertex v is processed, all counts from its predecessors have already been added. The final answer ways[t] is the number of s→t paths modulo 1e9+7.

Time: O(n + m)Space: O(n + m)
Shortest Path from Every Vertex to a Fixed Sink t (Backward DP on a Weighted DAG)
1#include <bits/stdc++.h>
2using namespace std;
3
4// Compute the shortest distance from every vertex to a given sink t in a weighted DAG.
5// We process vertices in reverse topological order and relax incoming states via outgoing edges.
6
7int main() {
8 ios::sync_with_stdio(false);
9 cin.tie(nullptr);
10
11 int n, m; // number of vertices and edges
12 // Example input:
13 // 6 7
14 // 1 2 2
15 // 1 3 1
16 // 2 4 3
17 // 2 5 4
18 // 3 5 2
19 // 4 6 1
20 // 5 6 3
21 // 6
22 if (!(cin >> n >> m)) return 0;
23
24 struct Edge { int to; long long w; };
25 vector<vector<Edge>> g(n + 1);
26 vector<int> indeg(n + 1, 0);
27 for (int i = 0; i < m; ++i) {
28 int u, v; long long w; cin >> u >> v >> w;
29 g[u].push_back({v, w});
30 indeg[v]++;
31 }
32 int t; cin >> t; // sink vertex
33
34 // Topological sort (Kahn)
35 queue<int> q;
36 for (int v = 1; v <= n; ++v) if (indeg[v] == 0) q.push(v);
37 vector<int> topo; topo.reserve(n);
38 while (!q.empty()) {
39 int u = q.front(); q.pop();
40 topo.push_back(u);
41 for (auto &e : g[u]) if (--indeg[e.to] == 0) q.push(e.to);
42 }
43 if ((int)topo.size() != n) {
44 cout << "The graph is not a DAG (cycle detected)." << '\n';
45 return 0;
46 }
47
48 const long long INF = (long long)4e18;
49 vector<long long> dist(n + 1, INF);
50 dist[t] = 0; // base case
51
52 // Process in reverse topological order: from later to earlier
53 for (int i = (int)topo.size() - 1; i >= 0; --i) {
54 int u = topo[i];
55 for (auto &e : g[u]) {
56 if (dist[e.to] != INF) {
57 dist[u] = min(dist[u], e.w + dist[e.to]);
58 }
59 }
60 }
61
62 // Output shortest distances to t (INF means unreachable)
63 for (int v = 1; v <= n; ++v) {
64 if (dist[v] == INF) cout << "INF\n";
65 else cout << dist[v] << '\n';
66 }
67
68 return 0;
69}
70

This program finds a topological order, initializes dist[t] = 0, and then walks the order backward. For each u, it relaxes dist[u] via all outgoing edges u→v using dist[v]. Acyclicity ensures no cycles interfere, and negative weights are safe because there are no cycles to create infinite descent.

Time: O(n + m)Space: O(n + m)