DSA DSA In Depth
0%
lesson dag-dptopological-sortdynamic-programming

DP on DAGs: Longest Path, Path Counting & Topological State Propagation

7 min read Module 6.2: Dynamic Programming Foundations & Classical Archetypes Updated Oct 23, 2026
📄 Cheat Sheet
Dynamic Programming on Directed Acyclic Graphs (DAGs) & Shortest/Longest Paths
WilliamFiset Dynamic Programming on Directed Acyclic Graphs (DAGs) & Shortest/Longest Paths Watch on YouTube ↗

Shows how Kahn's topological sort and DAG relaxation solve Longest Path, Shortest Path with negative weights, and Path Counting in linear time.

0:00 / --:--

1. Linear-Time Semiring Relaxation on DAGs

Given a Directed Acyclic Graph $G = (V, E)$ with topological ordering $\pi = (v_1, v_2, \dots, v_n)$ such that every directed edge $(u, v) \in E$ satisfies $\text{pos}(u) < \text{pos}(v)$, we can evaluate any associative path algebra in $O(V + E)$ time:

  • Shortest Path (even with negative edge weights!): $(\min, +)$ semiring: $dp[v] = \min_{u \to v} (dp[u] + w(u, v))$.
  • Longest Path (Critical Path Method / Project Scheduling): $(\max, +)$ semiring: $dp[v] = \max_{u \to v} (dp[u] + w(u, v))$.
  • Number of Distinct $s \leadsto v$ Walks: $(+, \times)$ semiring: $\text{ways}[v] = \sum_{u \to v} \text{ways}[u] \pmod{M}$.
  • Path Expectation / Probability: If leaving $u$ chooses an out-edge with probability $p(u, v)$, then $\mathbb{E}[\text{len}(u)] = \sum_{u \to v} p(u, v) \cdot (w(u, v) + \mathbb{E}[\text{len}(v)])$ evaluated in reverse topological order.

Core Invariant — Kahn's In-Degree Peeling vs. Source Reachability Contamination

When solving Single-Source Longest Path from a specific start vertex $s$ on a DAG, other vertices $r \neq s$ may also have in-degree $0$! If you initialize all vertices to dp[v] = 0 and run Kahn's algorithm from all in-degree-$0$ vertices, paths starting at $r \neq s$ will contaminate dp[v].
Rule: For single-source queries from $s$, initialize dp[s] = 0 and dp[v] = -INF for all $v \neq s$. Still peel all in-degree-$0$ vertices in Kahn's queue so the topological dependencies unlock properly, and guard edge relaxations with if (dp[u] != -INF)!

2. Extending DAG DP to General Directed Graphs via SCC Condensation

What if the input graph $G$ contains directed cycles, and we want to maximize the sum of vertex weights collected along a walk (where each vertex's weight is collected at most once)? Every Strongly Connected Component (SCC) $C_i$ allows visiting all vertices inside $C_i$ and exiting through any outgoing edge! Thus:

  1. Run Tarjan's or Kosaraju's algorithm in $O(V + E)$ to partition $G$ into SCCs $C_1, \dots, C_k$.
  2. Contract each SCC $C_i$ into a super-node with weight $W(C_i) = \sum_{u \in C_i} w_u$.
  3. The quotient graph $G^{\text{SCC}}$ is a guaranteed DAG! Run $O(V + E)$ Longest Path DP on $G^{\text{SCC}}$.
flowchart LR subgraph Kahn["Kahn's Topological Relaxation from Source s"] In0["Other Source r (in_deg=0)
dp[r] = -INF"] -->|"Unlocks in_deg of u,
does NOT relax dp[u]"| U["Vertex u
dp[u] = 10"] S["Start Source s
dp[s] = 0"] -->|"+10"| U U -->|"+7"| T["Target t
dp[t] = 17"] end

Hardware Reality — Reordering Vertex IDs into Topological Order for Cache Streaming

In large sparse DAGs (such as neural network computation graphs or static timing analysis circuits with $10^7$ gates), vertex IDs are often numbered arbitrarily, so dp[v] = max(dp[v], dp[u] + w) triggers random memory writes across dp[]. Relabeling vertices so that new ID $i$ is the $i$-th vertex in topological order turns dp[u] reads into monotonically forward memory accesses and guarantees every edge $(i, j)$ satisfies $i < j$.

3. Complexity Matrix

DP State DAG, 0/1 Knapsack Reverse Sweep & Myers Diff
O(N·W) / O(N·D) Edit Graph

Observe why 0/1 Knapsack iterates capacity w downwards (preventing item reuse), how Patience Sorting finds LIS in O(N log N), and how Myers Diff slides along diagonal snakes k = x - y.

Step 1 / 8
Custom Input:
Timeline

Invariant & State

Item 1 (wt=3, val=4): Sweep w = 7 downTo 3 so dp[w - 3] always reads the previous item layer.
🔗 Live Pointer & State Transition Graph — DP State DAG, 0/1 Knapsack Reverse Sweep & Myers Diff Active Pointer Jump/Transition Matched/Optimal
w=0 0 [0] 0 [1] 0 [2] wt=3, v 4 [3] 4 [4] 4 [5] 4 [6] dp[7]=4 4 [7]
[0]0w=0
[1]0
[2]0
[3]4wt=3, val=4
[4]4
[5]4
[6]4
[7]4dp[7]=4
Item 1
weight=3, value=4
Sweep Direction
w = 7 → 3 (Reverse)
Best dp[7]
4
[Step 1] Initialized 1D table with Item 1 (wt=3, val=4) across capacities 3..7.
Graph Problem On a General Graph On a DAG Space Complexity Method
Shortest Path (Negative Weights, No Neg Cycles) O(V * E) (Bellman-Ford) O(V + E) O(V) Topological $(\min, +)$ relaxation
Longest Simple Path NP-Hard O(V + E) O(V) Topological $(\max, +)$ relaxation
Count Distinct $s \leadsto t$ Walks #P-Complete (Simple) O(V + E) O(V) Topological $(+, \times)$ propagation
Reachability Count (Bitset Approximate/Exact) O(V * E / 64) ( via SCC) O(V * E / 64) O(V^2 / 64) Reverse topo reach[u] |= reach[v]

4. Reference C++20 Implementation

#include <algorithm>
#include <cstdint>
#include <queue>
#include <vector>

struct DirectedEdge {
    int to;
    long long weight;
};

struct DagPathMetrics {
    bool is_acyclic;
    long long longest_path;   // -INF if t is unreachable from s
    long long num_paths_mod;  // Number of distinct s -> t paths modulo 10^9 + 7
};

// Simultaneous Cycle Detection, Single-Source Longest Path, and Path Counting on a DAG
DagPathMetrics solve_dag_s_to_t(
    int n, const std::vector<std::vector<DirectedEdge>>& adj, int s, int t) {
    constexpr long long NEG_INF = -(1LL << 60);
    constexpr long long MOD = 1'000'000'007LL;

    std::vector<int> in_deg(n, 0);
    for (int u = 0; u < n; ++u) {
        for (const auto& e : adj[u]) ++in_deg[e.to];
    }

    // Push ALL in-degree 0 vertices so Kahn's topological peeling unlocks the entire DAG
    std::queue<int> q;
    for (int u = 0; u < n; ++u) {
        if (in_deg[u] == 0) q.push(u);
    }

    std::vector<long long> dist(n, NEG_INF);
    std::vector<long long> ways(n, 0);
    dist[s] = 0;
    ways[s] = 1;

    int visited_count = 0;
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        ++visited_count;

        for (const auto& e : adj[u]) {
            if (dist[u] != NEG_INF) {
                dist[e.to] = std::max(dist[e.to], dist[u] + e.weight);
                ways[e.to] = (ways[e.to] + ways[u]) % MOD;
            }
            if (--in_deg[e.to] == 0) {
                q.push(e.to);
            }
        }
    }

    if (visited_count != n) {
        return { false, NEG_INF, 0 }; // Graph contains a directed cycle!
    }
    return { true, dist[t], ways[t] };
}

Production Pitfall — Pushing Only s into Kahn's Queue Instead of All in_deg == 0 Nodes

A frequent bug in Single-Source DAG DP is initializing Kahn's queue with q.push(s) instead of pushing all vertices with in_deg[u] == 0. If a vertex $v$ is reachable from $s$ (e.g., $s \to v$) AND also has an incoming edge from an unreachable source $r \to v$, then $v$'s in_deg starts at $2$. If $r$ is never processed, in_deg[v] only drops from $2$ to $1$ and $v$ is never pushed into the queue! Always push all in_deg == 0 vertices (or run a reachability DFS from $s$ first to restrict in_deg to the reachable subgraph).

Check yourself Question 1 of 4
Concept Check Shortest and Longest Paths on DAGs with Negative Weights recall

Given a Directed Acyclic Graph (DAG) with $V$ vertices and $E$ directed edges whose weights may be positive, zero, or **negative**, what is the optimal time complexity to find the Single-Source Shortest Path (or Longest Path) from source $s$?

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

You are given a directed graph $G = (V, E)$ (which **may contain cycles**) and non-negative vertex profits $p_u \ge 0$. You may walk along directed edges and collect each visited vertex's profit at most once (revisiting vertices is allowed, but profit is counted once). How can you find the maximum profit walk in $O(V + E)$ time?

Key Takeaways

On a DAG, Shortest Path (even with negative weights), Longest Path, and Path Counting all run in O(V + E) time by relaxing edges in topological order.
For single-source queries from s, initialize dist[s] = 0 and dist[v] = -INF for v != s, while still peeling all in-degree 0 vertices in Kahn's queue.
Kahn's algorithm simultaneously validates acyclicity: if visited_count < V upon termination, the graph contains a directed cycle.
Vertex-weighted longest walk on a cyclic directed graph reduces to DAG DP after condensing Strongly Connected Components (SCCs) via Tarjan's algorithm.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue