10.8 Finding Shortest Paths in Directed Acyclic Graphs
Many shortest-path algorithms are designed to handle arbitrary graphs.
10.8 Finding Shortest Paths in Directed Acyclic Graphs
Many shortest-path algorithms are designed to handle arbitrary graphs. They must deal with cycles, repeated visits, and complex path structures. Directed acyclic graphs (DAGs) are different. Since DAGs contain no cycles, shortest paths become significantly easier to compute.
In fact, shortest paths in a DAG can be found in linear time.
This is faster than Dijkstra, faster than Bellman-Ford, and often simpler to implement. The absence of cycles removes many of the difficulties that make general shortest-path problems challenging.
This recipe shows how to exploit topological ordering to compute shortest paths efficiently in weighted DAGs.
Problem
Given a weighted directed acyclic graph and a source vertex, compute the shortest distance to every reachable vertex.
Consider the following graph:
5
A -----> B
| \\n |2 \1
| \\n v v
C -----> D -----> E
3 2
Starting from A, determine the minimum path cost to every vertex.
Unlike Dijkstra, edge weights may be negative as long as no cycles exist.
Solution
The algorithm consists of two phases:
- Compute a topological ordering.
- Process vertices in topological order while relaxing outgoing edges.
package main
import "fmt"
type Edge struct {
To int
Weight int
}
func shortestPathDAG(graph [][]Edge, source int) []int {
const INF = int(1e18)
n := len(graph)
visited := make([]bool, n)
order := make([]int, 0, n)
var dfs func(int)
dfs = func(v int) {
visited[v] = true
for _, edge := range graph[v] {
if !visited[edge.To] {
dfs(edge.To)
}
}
order = append(order, v)
}
for v := 0; v < n; v++ {
if !visited[v] {
dfs(v)
}
}
for i, j := 0, len(order)-1; i < j; i, j = i+1, j-1 {
order[i], order[j] = order[j], order[i]
}
dist := make([]int, n)
for i := range dist {
dist[i] = INF
}
dist[source] = 0
for _, v := range order {
if dist[v] == INF {
continue
}
for _, edge := range graph[v] {
candidate := dist[v] + edge.Weight
if candidate < dist[edge.To] {
dist[edge.To] = candidate
}
}
}
return dist
}
func main() {
graph := [][]Edge{
{{1, 5}, {2, 2}},
{{4, 1}},
{{3, 3}},
{{4, 2}},
{},
}
fmt.Println(shortestPathDAG(graph, 0))
}
Output:
[0 5 2 5 6]
The shortest path to vertex E has cost:
A -> B -> E
with total cost:
5 + 1 = 6
Understanding Topological Order
A topological ordering arranges vertices so that every edge points forward.
For example:
A -> B
A -> C
C -> D
B -> E
D -> E
One valid topological order is:
A C B D E
Every edge moves from left to right.
No edge ever points backward.
This property is possible only because DAGs contain no cycles.
The ordering guarantees that when a vertex is processed, every path that can reach it has already been considered.
Why This Solves Shortest Paths
Suppose we process vertices in topological order.
A
C
B
D
E
When processing:
D
every possible predecessor of D has already been processed.
No future vertex can create a new path back to D.
That would require a cycle.
Consequently, once all incoming edges have been relaxed, the distance to D is final.
This reasoning applies to every vertex.
The algorithm effectively performs a single forward sweep through the graph.
Visualizing the Relaxations
Consider:
A -> B = 4
A -> C = 1
C -> B = 2
Initial distances:
A = 0
B = ∞
C = ∞
Process A:
A = 0
B = 4
C = 1
Process C:
candidate = 1 + 2 = 3
Update:
B = 3
Process B:
No further changes.
Final result:
A = 0
B = 3
C = 1
The shortest path to B is:
A -> C -> B
rather than the direct edge.
Supporting Negative Edges
A useful property of DAG shortest paths is support for negative edge weights.
Consider:
A -> B = 5
A -> C = 2
C -> B = -4
The shortest path becomes:
A -> C -> B
with cost:
2 + (-4) = -2
Dijkstra cannot safely process this graph.
The DAG algorithm handles it naturally.
The reason is simple:
No cycles exist.
Without cycles, negative weights cannot produce endlessly improving paths.
The topological ordering remains valid.
Path Reconstruction
As with Dijkstra and Bellman-Ford, store predecessors during relaxation.
parent := make([]int, n)
for i := range parent {
parent[i] = -1
}
Whenever a shorter path is found:
dist[next] = candidate
parent[next] = current
After processing finishes, reconstruct paths by following parent links backward.
This produces the actual shortest route rather than only its cost.
Comparison with Dijkstra
Suppose:
V = 1,000,000
E = 2,000,000
Dijkstra requires:
O((V + E) log V)
operations.
A DAG shortest-path algorithm requires:
O(V + E)
operations.
The difference can be substantial for large graphs.
Whenever the graph is known to be acyclic, exploiting that structure is usually worthwhile.
Discussion
The algorithm combines two important ideas:
- Topological sorting
- Edge relaxation
The topological sort removes the need for repeated processing.
The relaxation step remains identical to Bellman-Ford and Dijkstra.
This demonstrates an important principle in graph algorithms:
Relaxation is often the core operation. Different shortest-path algorithms differ primarily in the order in which relaxations occur.
Bellman-Ford relaxes all edges repeatedly.
Dijkstra relaxes edges in increasing distance order.
DAG shortest paths relax edges in topological order.
The underlying operation remains the same.
Complexity
Let:
V= number of verticesE= number of edges
Topological sorting:
O(V + E)
Shortest-path computation:
O(E)
Total:
| Operation | Complexity |
|---|---|
| Topological sort | O(V + E) |
| Relaxation pass | O(E) |
| Total runtime | O(V + E) |
| Memory usage | O(V + E) |
This is optimal for graph traversal problems because every vertex and edge must be examined at least once.
Common Mistakes
Do not apply this algorithm to arbitrary directed graphs.
Topological ordering exists only for DAGs.
Do not assume all DFS orders are topological orders. The vertices must be processed in reverse finishing order.
Do not ignore disconnected components when constructing the ordering.
Do not reject negative edge weights automatically. They are perfectly valid in DAGs.
Do not forget reachability checks:
if dist[v] == INF {
continue
}
Otherwise arithmetic involving infinity values may produce incorrect results.
Practical Applications
DAG shortest paths appear in:
- Build systems
- Dependency resolution
- Workflow orchestration
- Project scheduling
- Data processing pipelines
- Compiler optimization passes
- Task execution graphs
- Manufacturing workflows
Many real-world dependency graphs are naturally acyclic, making this algorithm especially useful.
Takeaway
Directed acyclic graphs permit a much simpler shortest-path solution than general graphs. By processing vertices in topological order, every vertex is finalized exactly once, producing shortest paths in linear time. The algorithm supports negative edge weights, avoids priority queues entirely, and illustrates how exploiting graph structure can lead to dramatically more efficient solutions.