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:
- Run Tarjan's or Kosaraju's algorithm in $O(V + E)$ to partition $G$ into SCCs $C_1, \dots, C_k$.
- Contract each SCC $C_i$ into a super-node with weight $W(C_i) = \sum_{u \in C_i} w_u$.
- The quotient graph $G^{\text{SCC}}$ is a guaranteed DAG! Run $O(V + E)$ Longest Path DP on $G^{\text{SCC}}$.
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
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.
Invariant & State
| 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).