13.21 Dynamic Programming on Graphs
Dynamic programming is easiest when subproblems form a simple order: left to right, bottom to top, short interval to long interval, child before parent.
13.21 Dynamic Programming on Graphs
Dynamic programming is easiest when subproblems form a simple order: left to right, bottom to top, short interval to long interval, child before parent. Graphs complicate this picture. A graph may contain many paths into the same state, and cycles may destroy any obvious computation order.
Dynamic programming on graphs works when the graph structure still allows dependencies to be evaluated safely. The most common case is a directed acyclic graph, usually called a DAG. In a DAG, states can be processed in topological order, guaranteeing that every dependency is computed before it is used.
This section shows how to treat graph vertices as DP states, graph edges as transitions, and topological order as the evaluation plan.
Problem
How do we apply dynamic programming when the states and transitions form a graph?
The Basic Model
A graph DP usually has:
$$ dp[u] $$
meaning:
the best value, count, or property associated with vertex (u).
Edges define transitions.
If there is an edge:
$$ u \rightarrow v $$
then information can flow from (u) to (v), or from (v) back to (u), depending on the recurrence.
Examples:
number of paths to each node
longest path ending at each node
shortest distance in a DAG
ways to complete a dependency graph
The graph itself becomes the dependency structure.
Why DAGs Matter
Dynamic programming requires an acyclic dependency order.
If:
A depends on B
B depends on C
C depends on A
then none of the states can be computed first.
A directed acyclic graph avoids this problem. It has at least one topological ordering: a sequence of vertices where every directed edge goes from earlier to later.
For example:
A -> B -> D
A -> C -> D
A valid topological order is:
A, B, C, D
Once vertices are processed in this order, every forward dependency is safe.
Recipe: Count Paths in a DAG
Suppose we want the number of paths from a source vertex (s) to every other vertex.
State:
$$ dp[u] $$
Meaning:
number of paths
from s to u
Base case:
$$ dp[s]=1 $$
Transition:
For every edge:
$$ u \rightarrow v $$
add:
$$ dp[u] $$
to:
$$ dp[v] $$
Implementation:
func CountPathsDAG(
graph [][]int,
order []int,
source int,
) []int {
n := len(graph)
dp := make([]int, n)
dp[source] = 1
for _, u := range order {
for _, v := range graph[u] {
dp[v] += dp[u]
}
}
return dp
}
The key requirement is that order must be topological.
Recipe: Longest Path in a DAG
The longest path problem is hard in general graphs, but straightforward in DAGs.
State:
$$ dp[u] $$
Meaning:
length of the longest path
ending at u
Initialize:
dp[u] = 0
for all vertices.
For every edge:
$$ u \rightarrow v $$
update:
$$ dp[v] = \max(dp[v], dp[u]+1) $$
Implementation:
func LongestPathDAG(
graph [][]int,
order []int,
) int {
n := len(graph)
dp := make([]int, n)
answer := 0
for _, u := range order {
for _, v := range graph[u] {
dp[v] = max(
dp[v],
dp[u]+1,
)
answer = max(answer, dp[v])
}
}
return answer
}
This is the same relaxation pattern used in shortest paths, but with max instead of min.
Recipe: Shortest Paths in a DAG
For weighted DAGs, shortest paths can be computed in one topological pass.
State:
$$ dp[u] $$
Meaning:
shortest distance
from source to u
Base case:
$$ dp[source]=0 $$
All other states begin at infinity.
For edge:
$$ u \rightarrow v $$
with weight:
$$ w $$
transition:
$$ dp[v] = \min(dp[v], dp[u]+w) $$
Implementation:
type Edge struct {
To int
Weight int
}
func ShortestPathDAG(
graph [][]Edge,
order []int,
source int,
) []int {
const inf = int(1e9)
n := len(graph)
dp := make([]int, n)
for i := range dp {
dp[i] = inf
}
dp[source] = 0
for _, u := range order {
if dp[u] == inf {
continue
}
for _, e := range graph[u] {
dp[e.To] = min(
dp[e.To],
dp[u]+e.Weight,
)
}
}
return dp
}
Unlike Dijkstra, this works with negative edge weights as long as the graph is acyclic.
Topological Sorting
A topological order can be computed with DFS or indegree counting.
Kahn's algorithm is often the most convenient.
func TopologicalOrder(graph [][]int) []int {
n := len(graph)
indegree := make([]int, n)
for u := 0; u < n; u++ {
for _, v := range graph[u] {
indegree[v]++
}
}
queue := []int{}
for u := 0; u < n; u++ {
if indegree[u] == 0 {
queue = append(queue, u)
}
}
order := []int{}
for len(queue) > 0 {
u := queue[0]
queue = queue[1:]
order = append(order, u)
for _, v := range graph[u] {
indegree[v]--
if indegree[v] == 0 {
queue = append(queue, v)
}
}
}
return order
}
If the resulting order contains fewer than (n) vertices, the graph contains a cycle.
Pull Versus Push Transitions
Graph DP can be written in two styles.
Push Style
Process a state and update its neighbors.
for _, u := range order {
for _, v := range graph[u] {
dp[v] = combine(dp[v], transition(dp[u], v))
}
}
This style is common for path counting and shortest paths.
Pull Style
Compute a state from its predecessors.
for _, u := range order {
for _, p := range reverseGraph[u] {
dp[u] = combine(dp[u], transition(dp[p], u))
}
}
This style is useful when the recurrence is naturally defined in terms of incoming edges.
Both are equivalent when implemented carefully.
Handling Multiple Sources
Some graph DP problems have many starting states.
Example:
longest path in any component
In that case, initialize every source appropriately.
For counting paths:
dp[source] = 1
for each source.
For shortest paths:
dp[source] = 0
for each source.
Multiple-source initialization is often cleaner than adding an artificial super-source.
Cycles and Strongly Connected Components
Cycles break simple DP because dependencies can become circular.
For example:
A -> B -> C -> A
The state values may depend on themselves.
There are several possible responses:
- reject the input if a DAG is required
- collapse strongly connected components
- use graph algorithms such as Dijkstra or Bellman-Ford
- solve equations if the recurrence is probabilistic
- use iterative relaxation when convergence is guaranteed
The right choice depends on the problem.
Dynamic programming on graphs is simplest and safest on DAGs.
Condensation Graphs
A directed graph can be compressed into strongly connected components. Each component becomes a node. The resulting condensation graph is always a DAG.
This is useful when the recurrence works at the component level.
For example:
inside each strongly connected component:
handle separately
between components:
run DP on the condensation DAG
This pattern appears in reachability, implication graphs, dependency analysis, and compiler optimization.
Common Patterns
Path Counting
$$ dp[v] += dp[u] $$
for every edge (u \rightarrow v).
Longest Path
$$ dp[v] = \max(dp[v], dp[u]+w) $$
Shortest Path in DAG
$$ dp[v] = \min(dp[v], dp[u]+w) $$
Feasibility
$$ dp[v] = dp[v] \lor dp[u] $$
Number of Ways to Finish
Process reverse topological order:
$$ dp[u] = \sum_{u \rightarrow v} dp[v] $$
Example: Course Completion Count
Suppose a course dependency graph is a DAG. An edge:
u -> v
means course u must be completed before course v.
You may want to count the number of ways to reach each course from prerequisite roots, or compute the longest prerequisite chain.
The longest prerequisite chain is exactly longest path in a DAG.
State:
$$ dp[v] $$
Meaning:
longest chain ending at course v
Transition:
$$ dp[v] = \max(dp[v], dp[u]+1) $$
for every prerequisite edge.
Common Mistakes
Forgetting to Topologically Sort
Processing vertices in numeric order only works if numeric order happens to be topological.
Do not assume it.
Ignoring Cycles
If the graph contains cycles, a single topological pass is invalid.
Detect cycles explicitly.
Wrong Edge Direction
If dependencies are reversed, the recurrence computes a different quantity.
Be precise about whether:
u -> v
means:
u leads to v
or:
u depends on v
Bad Initialization
For counting paths, 0 usually means no known paths.
For shortest paths, unreachable states must begin at infinity.
For longest paths, initialization depends on whether empty paths are allowed.
Complexity Analysis
Let:
$$ V $$
be the number of vertices.
Let:
$$ E $$
be the number of edges.
Topological sorting costs:
$$ O(V+E) $$
A single DP pass also costs:
$$ O(V+E) $$
Memory usage:
$$ O(V+E) $$
including the graph.
This makes DAG dynamic programming highly efficient.
Recognizing Graph DP
Strong indicators include:
dependencies
directed acyclic graph
topological order
number of paths
longest chain
workflow states
build systems
course prerequisites
task scheduling
If a problem can be modeled as states connected by acyclic transitions, graph DP should be considered.
Takeaway
Dynamic programming on graphs treats vertices as states and edges as transitions. When the graph is acyclic, topological order gives a safe computation order, allowing path counts, longest paths, shortest paths, feasibility, and dependency metrics to be computed in linear time. The method is powerful because it generalizes many earlier DP patterns: arrays, trees, intervals, and subsets can all be viewed as special dependency graphs. The main discipline is structural: verify the graph is acyclic, choose the correct edge direction, initialize base states carefully, and process states in dependency order.