Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Shortest Paths at Scale  ›  Lesson

Shortest Paths on a DAG

Shortest Paths on a DAG 26 minRelax in topological order and every cost settles exactly once
You're building a piece ofMeridian — the router
This piece — dag_route(): Finds the cheapest run through the one-way night timetable in a single sweep, credits and all.
Scenario Meridian's night shuttles run on a timetable that only moves forward in time, and some legs pay the van back for carrying return freight. The timetable router finds the cheapest run through that one-way network in a single sweep, credits and all.
Your task
Build dag_route(legs, start, goal). `legs` is a directed, acyclic edge list of [from, to, cost] for Meridian's night timetable; a cost may be negative (a credit). Return {"cost": total, "path": stops} for the cheapest route from `start` to `goal` (every test network has exactly one cheapest route), or {"cost": None, "path": []} if `goal` can't be reached. Example: dag_route([["depot", "quay", 5], ["depot", "mill", 2], ["quay", "yard", -4], ["mill", "yard", 3], ["yard", "harbour", 1]], "depot", "harbour") → {"cost": 2, "path": ["depot", "quay", "yard", "harbour"]}.

Shortest Paths on a DAG

In short: on a directed acyclic graph (DAG) — where legs only ever point "forward", with no way back to a stop already visited — one sweep in topological order finds every cheapest cost, even with negative edges, in O(V + E). Process stops in an order where every leg into a stop comes from an earlier stop, and by the time you reach a stop every leg that could lower its cost has already run. No heap, no settling order to argue about: the order itself is the proof.

Six stops laid out left to right in topological order, each badge settling once as legs point only forward, including one negative-cost leg from quay to yard

Meridian has a model of the city. This section routes things through it and builds the router, starting with the night timetable. After midnight, shuttles run between depots on fixed times, and time only moves forward, so no chain of shuttles can bring you back to where you started. The timetable is a directed acyclic graph, a DAG.

The twist: some legs carry return freight that Meridian is paid for, so on the books they have a negative cost, a credit. You saw a negative edge fool Dijkstra in the graphs section. On a DAG you don't need Dijkstra at all: one sweep through the stops, in the right order, finds every cheapest cost, credits included, in O(V + E).

Where Dijkstra goes wrong

Dijkstra settles the cheapest unsettled stop and never looks at it again. The promise behind that is "nothing found later can be cheaper", and a credit breaks it. Here the mill is settled at 2, but the route through the quay reaches it for 5 − 4 = 1:

What does this print?
import heapq

legs = {"depot": [("mill", 2), ("quay", 5)], "quay": [("mill", -4)],
        "mill": [("harbour", 3)], "harbour": []}
settled = set()
best = {"depot": 0}
heap = [(0, "depot")]
while heap:
    cost, stop = heapq.heappop(heap)
    if stop in settled:
        continue
    settled.add(stop)
    for nxt, step in legs[stop]:
        if nxt not in settled and cost + step < best.get(nxt, float("inf")):
            best[nxt] = cost + step
            heapq.heappush(heap, (cost + step, nxt))
print(best["harbour"])

Dijkstra guessed which stop was final from costs. With no cycles there is a better source of truth: the order of the stops.

The order is the argument

Put the stops in topological order, so that every leg runs from an earlier stop to a later one. You built this order in the graph structure section with Kahn's algorithm: count the legs into each stop, emit a stop when its count reaches zero, and lower the counts of the stops it points at.

Now look at any stop in that order. Every leg into it comes from a stop earlier in the order. So if you process the stops in order and relax every leg out of each one, then by the time you reach a stop, every leg that could lower its cost has already been relaxed. Its cost is final. It doesn't matter whether any of those legs were credits. No later discovery can change it, because nothing later points back.

That is the whole algorithm:

  1. Find a topological order.
  2. Set the start to 0 and every other stop to unknown.
  3. Walk the order. For each stop with a known cost, relax each leg out of it.

Watch it on the night network. The stops are drawn in rows by their position in the order, and each badge is the cheapest cost known so far. A settled badge never changes again:

watch it move · graph

In topological order, every leg into a stop is relaxed before the stop is reached, so each badge settles once and never moves again, credits and all.

The depot comes first in the order and costs nothing to reach. 6 nodes, 8 edges, directed. At depot. Settled: depot. depot: 0.

order: depot, quay, mill, yard, market, harbour

1 / 7

The mill was processed after the quay even though it is cheaper to reach. The sweep picks what to process next by order alone, never by cost.

The sweep in code

The order comes from Kahn's algorithm, and the sweep is a single loop over it:

python

Your order may differ from the picture's. Any valid order works; the argument only needed "every leg points forward".

How much work is that? Kahn's algorithm looks at every leg once, and so does the sweep.

What does this print?
out = {
    "depot": [("quay", 5), ("mill", 2)], "quay": [("yard", -4)],
    "mill": [("yard", 3), ("market", 4)], "yard": [("market", 1), ("harbour", 6)],
    "market": [("harbour", 2)], "harbour": [],
}
order = ["depot", "quay", "mill", "yard", "market", "harbour"]
best, checks = {"depot": 0}, 0
for stop in order:
    for nxt, cost in out[stop]:
        checks += 1
        if best[stop] + cost < best.get(nxt, float("inf")):
            best[nxt] = best[stop] + cost
print(checks)

The order, the waiting counts and the best costs are each one entry per stop or leg, so the sweep also costs O(V + E) space — no heap, and nothing extra to store beyond the graph itself.

Step through two stops of it by hand. Watch the yard's cost get offered twice and keep the smaller one:

step through it
1order = ["depot", "quay", "mill", "yard"]
2out = {"depot": [("quay", 5), ("mill", 2)], "quay": [("yard", -4)], "mill": [("yard", 3)], "yard": []}
3dist = {"depot": 0}
4for stop in order:
5 for nxt, cost in out[stop]:
6 offer = dist[stop] + cost
7 if offer < dist.get(nxt, 99):
8 dist[nxt] = offer
9print(dist)

Order is not optional

The sweep's correctness comes entirely from the order. Relax the same legs in the order they happen to be listed, and a stop can pass on its cost before that cost is final:

broken — fix it

The cheapest route is depot → quay → yard → harbour, costing 5 − 4 + 1 = 2, but this prints 4. Make the relaxation follow the topological order.

Your sweep, drawn

Here is the whole thing, Kahn's order then the sweep, drawing each relaxation. Try giving the mill → yard leg a credit of its own, say -2, and see which route the harbour takes. Or add a leg that closes a loop, like ["harbour", "depot", 1], and watch what the order does to the stops on it.

your code, drawn · graph

Your sweep. A stop is only processed once everything pointing at it is done, so its badge is final when its turn comes.

If you added the loop, the stops on it never reach an in-degree of zero, and they never get processed at all. The sweep is only defined on a DAG, and Kahn's algorithm is also how you find out whether you have one.

It's dynamic programming wearing a map

Write down what the cheapest cost to a stop is:

best(v)=min⁡(u, v, w)(best(u)+w),best(start)=0\text{best}(v) = \min_{(u,\,v,\,w)} \big(\text{best}(u) + w\big), \qquad \text{best}(\text{start}) = 0

That's a recurrence, exactly like the ones in the dynamic programming lessons. Each subproblem, "cheapest cost to u", is reused by every leg leaving u. The DAG is the dependency graph of the subproblems, and the topological order is the order you fill the table in. Written top-down with a cache, it is the same algorithm:

python

The recursion finds its own order; the sweep writes it down in advance. Any problem whose states form a DAG is a shortest-path problem in disguise.

Nothing here depends on the sign of a cost, so swapping min for max gives the longest route through a DAG, a problem with no known fast algorithm on general graphs.

Why brute force isn't an option

Suppose each hour has 4 stops, each with a shuttle to every stop in the next hour. There are 4 to the power of the number of hours routes to price, but the sweep relaxes just 16 legs per hour:

hours routes to price legs the sweep relaxes
5 1,024 80
10 1,048,576 160
12 16,777,216 192

A night timetable runs about ten hours, so brute force is out the moment the timetable is real.


Interview-style: the toll pass

Here's a problem in the shape interviewers like, worded for Meridian. A van crossing the night network carries a single toll pass. On one leg of its route it may use the pass to make that leg free. What is the cheapest possible run from the depot to the harbour?

The tempting answer is to find the cheapest route and waive its dearest leg. That fails when a slightly dearer route hides one huge leg. The reliable approach is to make the pass part of the state: every stop becomes two, pass held and pass spent. A held state can pay a leg (stay held) or waive it (become spent). The state graph is still a DAG, still in the same order, so one sweep solves it. The boss exercise asks you to build it.

What it costs

Algorithm Time Works when
DAG sweep (Kahn + relax) O(V + E) no cycles; any costs, including credits
Dijkstra with a heap O((V + E) log V) cycles allowed; costs never negative
Bellman-Ford (next lesson) O(V × E) cycles and credits; detects money-making loops

❓ Cross-question — "Roads are two-way, so where do DAGs actually show up?" Wherever one quantity only moves one way: time in a timetable, stages in a pipeline, days in a plan. Turn "stop" into "stop at a moment" and a cyclic map becomes an acyclic timetable, which is what the capstone exercise does.


Quick practice

What does this print?
out = {"a": [("b", 1), ("c", 4)], "b": [("c", -2)], "c": [("d", 3)], "d": []}
order = ["a", "b", "c", "d"]
best = {"a": 0}
for stop in order:
    if stop not in best:
        continue
    for nxt, cost in out[stop]:
        if best[stop] + cost < best.get(nxt, float("inf")):
            best[nxt] = best[stop] + cost
print(best["d"])
broken — fix itKeyError

c can't be reached from a, so the sweep should just skip it, but reading best["c"] crashes. Add the missing check.

🎯 Your turn

Write dag_route(legs, start, goal). legs is a directed, acyclic edge list of [from, to, cost], and costs may be negative. Return the cheapest route as {"cost": total, "path": stops}, or {"cost": None, "path": []} if goal can't be reached:

  • dag_route([["depot", "quay", 5], ["depot", "mill", 2], ["quay", "yard", -4], ["mill", "yard", 3], ["yard", "harbour", 1]], "depot", "harbour") → {"cost": 2, "path": ["depot", "quay", "yard", "harbour"]}
  • the same legs from "mill" to "mill" → {"cost": 0, "path": ["mill"]}

Hint: Kahn's algorithm for the order, one sweep with best and parent, then walk parent back from the goal.

Then press ▶ Run, tap the Live App try chips to call it with different inputs, and hit ✓ Check. Green = this piece of Meridian's router is built. ✅

Practice — level up

0/6 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upOne sweep, order supplied

Someone has already put Meridian's stops in topological order for you. Write sweep(order, legs, start): legs is a directed edge list of [from, to, cost] (costs may be negative), and every leg's from comes before its to in order. Walk order once, relaxing every leg out of each stop you have a cost for, and return a dict of the cheapest cost from start to every stop it can reach (including start at 0). Example: sweep(["depot", "quay", "yard"], [["depot", "quay", 5], ["quay", "yard", -4], ["depot", "yard", 3]], "depot") → {"depot": 0, "quay": 5, "yard": 1}.

relaxationweighted-graph
DrillEvery cheapest cost, no order supplied

Now find the order yourself. Write dag_distances(legs, start): legs is a directed, acyclic edge list of [from, to, cost], costs may be negative, and you return the cheapest cost from start to every stop it reaches, start included at 0. Relax each stop's legs as the stop leaves Kahn's queue. Example: [["depot", "mill", 2], ["depot", "quay", 5], ["quay", "mill", -4]] from "depot" → {"depot": 0, "quay": 5, "mill": 1}.

topological-sortrelaxation
BuildHow many cheapest routes?

Dispatch wants to know how much slack the night timetable has: if one route closes, is there another just as cheap? Write count_cheapest_routes(legs, start, goal) for a directed, acyclic edge list of [from, to, cost], returning how many different routes from start to goal achieve the cheapest cost. Return 0 if goal can't be reached, and 1 when start == goal.

dptopological-sortrelaxation
BossThe one-leg toll pass

An interview-style routing problem, in Meridian's clothes. A night van carries one toll pass, and on any single leg of its route it may use the pass to make that leg cost 0. It may also keep the pass unused. legs is a directed, acyclic edge list of [from, to, cost] (costs may be negative, and a pass spent on a credit leg only loses the credit). Write cheapest_with_pass(legs, start, goal) returning the cheapest possible cost from start to goal, or None if goal can't be reached. start == goal costs 0.

shortest-pathsdptopological-sort
CapstoneEarliest arrival on the timetable

Meridian's night shuttles run to a timetable. trips is a list of [from, to, depart, arrive] with arrive > depart. You are at start at time t0 and can board any trip that leaves a stop you are at, at or after the time you got there (changing at a stop takes no time). Write earliest_arrival(trips, start, goal, t0) returning the earliest time you can be at goal, t0 if you are already there, or -1 if you can't get there.

shortest-pathssortingrelaxation
ExplainWhy one sweep is enough

Dijkstra's algorithm can give the wrong answer when some legs carry a credit (negative cost), but one sweep in topological order never does, even with credits. Explain why the sweep is safe, and name the assumption Dijkstra makes that the sweep does not need.

dag_route.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
dag_route(legs, start, goal) → dictThe cheapest route through an acyclic timetable as {cost, path}; cost None and an empty path if unreachable.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.