Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Data Structures & Algorithms (DSA)  Module 12 – Shortest paths, spanning trees and flows

Shortest paths: relaxation and DAGs

What a shortest path is, why breadth-first search fails on weighted edges, how relaxation works, and how a DAG is solved in one pass, longest paths too.

  • Intermediate
  • 30 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free lesson, Pro answers: 30 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Define single-source shortest paths, distances and relaxation
  • Explain why breadth-first search fails on weighted graphs and when negative edges matter
  • Compute shortest and longest paths in a DAG in O(V + E) time
  • Choose a shortest-path algorithm from the weights and the shape of a graph

Before you start

What you’ll learn · Before you start

What you will learn

  • Define single-source shortest paths, distances and relaxation
  • Explain why breadth-first search fails on weighted graphs and when negative edges matter
  • Compute shortest and longest paths in a DAG in O(V + E) time
  • Choose a shortest-path algorithm from the weights and the shape of a graph

Before you start

On this page

A shortest path from a vertex s to a vertex v of a weighted graph is a path whose edge weights add up to the smallest possible total, and that total is the distance from s to v. The single-source problem asks for the distance from one source to every vertex, plus a way to rebuild each path. Every classic algorithm for it keeps an estimate dist[v] for each vertex and improves it with one step, relaxation: if going to u and then along the edge u → v is cheaper than the best route to v found so far, remember the cheaper total. The algorithms differ only in the order in which they relax edges. In a directed acyclic graph (DAG) the right order is known in advance: relax the edges in topological order, and one pass gives every distance in O(V + E), even with negative weights. The same pass with “longer” in place of “shorter” finds the longest path, the critical path of a project plan.

What a shortest path is

Take a directed graph whose edges carry weights: minutes, fares, metres, risk. The weight of a path is the sum of the weights of its edges, and the distance δ(s, v) is the smallest weight of any path from s to v. When no path leads from s to v, the distance is infinite, and code stores it as math.inf in Python or Infinity in JavaScript. An undirected road is two directed edges, one each way, so everything here covers undirected graphs too.

A solution returns two arrays. dist[v] holds the distance, and parent[v] the vertex before v on one shortest path, so the path to any vertex is read backwards from it to s through the parents. The parent edges together form a shortest-path tree rooted at s. One fact makes that tree possible: every part of a shortest path is itself a shortest path. If the best route from s to v passes through u, its first stretch is a best route from s to u, because a cheaper way to u would make a cheaper way to v.

Why breadth-first search is not enough

Breadth-first search (BFS) finds the path with the fewest edges, which is the shortest path only when every edge weighs the same. On a weighted map, a route with more roads can take fewer minutes:

A map of five places: one road from depot to market takes 9 minutes; three roads through school and bank take 7.depotschoolbankparkmarket9 min: fewest roads232: 7 min in all44

Fewest roads is not fewest minutes

Text description of the diagram

Five places are joined by one-way roads, each labelled with the minutes it takes.

  1. The direct road from the depot to the market takes 9 minutes. It is drawn dashed: breadth-first search picks it, because it is a single road.
  2. The route depot to school (2 minutes), school to bank (3) and bank to market (2) uses three roads and takes 7 minutes. It is drawn thick: it is the shortest path by weight.
  3. The third route, depot to school (2), school to park (4) and park to market (4), takes 10 minutes.
Fewest roads against fewest minutes on a rider's map Python · trap/bfs_trap.py
# A rider's map (made up): one-way roads between five places, weighted by minutes. Breadth-first search finds the
# route with the fewest roads; the route with the fewest minutes can use more roads.
from collections import deque
from itertools import count

roads = [
    ("depot", "market", 9),
    ("depot", "school", 2),
    ("school", "bank", 3),
    ("bank", "market", 2),
    ("school", "park", 4),
    ("park", "market", 4),
]
graph = {}
for u, v, minutes in roads:
    graph.setdefault(u, []).append((v, minutes))
    graph.setdefault(v, [])


def every_route(u, target, route):
    """All routes from u to target that repeat no place (fine for five places, hopeless for five thousand)."""
    if u == target:
        yield route
        return
    for v, _ in graph[u]:
        if v not in route:
            yield from every_route(v, target, route + [v])


def minutes_of(route):
    weight = {(u, v): m for u, v, m in roads}
    return sum(weight[step] for step in zip(route, route[1:]))


def bfs_route(graph, source, target):
    """Breadth-first search with parent pointers: the route with the fewest edges."""
    parent = {source: None}
    queue = deque([source])
    while queue:
        u = queue.popleft()
        for v, _ in graph[u]:
            if v not in parent:
                parent[v] = u
                queue.append(v)
    route = [target]
    while parent[route[-1]] is not None:
        route.append(parent[route[-1]])
    return route[::-1]


print("every route from the depot to the market:")
routes = list(every_route("depot", "market", ["depot"]))
for route in routes:
    print(f"  {' -> '.join(route):34} {len(route) - 1} road{'s' if len(route) > 2 else ' '}  {minutes_of(route):2} min")
fewest = bfs_route(graph, "depot", "market")
best = min(routes, key=minutes_of)
print(f"BFS, fewest roads: {' -> '.join(fewest)}, {minutes_of(fewest)} min")
print(f"fewest minutes:    {' -> '.join(best)}, {minutes_of(best)} min")

# The repair that works only for small whole-number weights: cut every road into 1-minute pieces, then run BFS.
fresh = count()
pieces = {place: [] for place in graph}
for u, v, minutes in roads:
    previous = u
    for _ in range(minutes - 1):
        stop = f"piece {next(fresh)}"
        pieces[stop] = []
        pieces[previous].append((stop, 1))
        previous = stop
    pieces[previous].append((v, 1))
cut = bfs_route(pieces, "depot", "market")
edges = sum(len(out) for out in pieces.values())
print(f"cut into 1-minute pieces: {len(pieces)} vertices and {edges} edges instead of {len(graph)} and {len(roads)};"
      f" BFS now needs {len(cut) - 1} steps, the {minutes_of(best)} minutes")

Output

every route from the depot to the market:
  depot -> market                    1 road    9 min
  depot -> school -> bank -> market  3 roads   7 min
  depot -> school -> park -> market  3 roads  10 min
BFS, fewest roads: depot -> market, 9 min
fewest minutes:    depot -> school -> bank -> market, 7 min
cut into 1-minute pieces: 23 vertices and 24 edges instead of 5 and 6; BFS now needs 7 steps, the 7 minutes

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bfs_trap.py

BFS took the single road of 9 minutes, while the three roads through the school and the bank take 7. One repair works for small whole-number weights: cut every edge of weight w into w edges of weight 1, and BFS on the bigger graph is correct. Here the map grew from 5 vertices and 6 edges to 23 and 24. With weights in seconds, or up to 10⁹ as in typical problem limits, the cut graph would not fit in memory, so weighted graphs need algorithms that work with the weights themselves. When every weight is 0 or 1, a deque instead of a queue (0-1 BFS) keeps the search at O(V + E).

Relaxation, the step every algorithm repeats

Start with dist[s] = 0 and every other dist[v] = ∞. Relaxing the edge u → v of weight w means:

  • if dist[u] + w < dist[v], set dist[v] = dist[u] + w and parent[v] = u;
  • otherwise, change nothing.

This trace relaxes the six edges of a small graph in two orders:

Relaxation step by step, in an unlucky order and in a topological order Python · relax/relax_trace.py
# Relaxation, one edge at a time, on a small directed graph. Every shortest-path algorithm of this module is this
# step repeated; they differ only in the order in which they pick the edges.
import math

edges = [("s", "a", 4), ("s", "b", 1), ("b", "a", 2), ("a", "c", 1), ("b", "c", 5), ("c", "d", 3)]


def show(x):
    return "inf" if x == math.inf else str(x)


def relax(dist, parent, u, v, w, log):
    """If going through u makes v cheaper, remember the cheaper distance and the edge that gave it."""
    if dist[u] + w < dist[v]:
        log.append(f"  relax {u} -> {v} ({w}): {show(dist[u])} + {w} = {dist[u] + w} < {show(dist[v])}, so dist[{v}] = {dist[u] + w}")
        dist[v] = dist[u] + w
        parent[v] = u
        return True
    log.append(f"  relax {u} -> {v} ({w}): {show(dist[u])} + {w} is not below {show(dist[v])}, no change")
    return False


def run(order, passes):
    dist = {v: math.inf for v in "sabcd"}
    parent = {v: None for v in "sabcd"}
    dist["s"] = 0
    relaxations = 0
    for p in range(1, passes + 1):
        log = []
        changed = False
        for u, v, w in order:
            changed |= relax(dist, parent, u, v, w, log)
            relaxations += 1
        if not changed:
            print(f" pass {p}: none of the {len(order)} edges changes anything, so the distances are final")
            break
        print(f" pass {p}:", *log, sep="\n")
    print(f" {relaxations} relaxations; dist: " + ", ".join(f"{v} {show(d)}" for v, d in dist.items()))
    route = ["d"]
    while parent[route[-1]] is not None:
        route.append(parent[route[-1]])
    print(f" shortest path to d, read back through the parents: {' -> '.join(reversed(route))}")


print("Order 1: the edges in an unlucky order, repeated until a pass changes nothing")
unlucky = [edges[i] for i in (3, 5, 0, 1, 2, 4)]
run(unlucky, passes=10)
print()
print("Order 2: the edges leaving s, then b, a, c (a topological order), one pass")
by_vertex = sorted(edges, key=lambda e: "sbacd".index(e[0]))
run(by_vertex, passes=1)

Output

Order 1: the edges in an unlucky order, repeated until a pass changes nothing
 pass 1:
  relax a -> c (1): inf + 1 is not below inf, no change
  relax c -> d (3): inf + 3 is not below inf, no change
  relax s -> a (4): 0 + 4 = 4 < inf, so dist[a] = 4
  relax s -> b (1): 0 + 1 = 1 < inf, so dist[b] = 1
  relax b -> a (2): 1 + 2 = 3 < 4, so dist[a] = 3
  relax b -> c (5): 1 + 5 = 6 < inf, so dist[c] = 6
 pass 2:
  relax a -> c (1): 3 + 1 = 4 < 6, so dist[c] = 4
  relax c -> d (3): 4 + 3 = 7 < inf, so dist[d] = 7
  relax s -> a (4): 0 + 4 is not below 3, no change
  relax s -> b (1): 0 + 1 is not below 1, no change
  relax b -> a (2): 1 + 2 is not below 3, no change
  relax b -> c (5): 1 + 5 is not below 4, no change
 pass 3: none of the 6 edges changes anything, so the distances are final
 18 relaxations; dist: s 0, a 3, b 1, c 4, d 7
 shortest path to d, read back through the parents: s -> b -> a -> c -> d

Order 2: the edges leaving s, then b, a, c (a topological order), one pass
 pass 1:
  relax s -> a (4): 0 + 4 = 4 < inf, so dist[a] = 4
  relax s -> b (1): 0 + 1 = 1 < inf, so dist[b] = 1
  relax b -> a (2): 1 + 2 = 3 < 4, so dist[a] = 3
  relax b -> c (5): 1 + 5 = 6 < inf, so dist[c] = 6
  relax a -> c (1): 3 + 1 = 4 < 6, so dist[c] = 4
  relax c -> d (3): 4 + 3 = 7 < inf, so dist[d] = 7
 6 relaxations; dist: s 0, a 3, b 1, c 4, d 7
 shortest path to d, read back through the parents: s -> b -> a -> c -> d

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 relax_trace.py

Three facts explain everything the algorithms of this module do. First, dist[v] is always the weight of some walk from s to v, or ∞, so it never drops below the true distance. Second, relaxation only ever lowers it, so once it equals the distance it stays there. Third, if the edges of a shortest path are relaxed in order, from s outwards, then the last vertex of that path has its true distance afterwards, whatever other relaxations happened in between. In the unlucky order, dist[c] first became 6 through b, and only the second pass, after dist[a] had dropped to 3, carried the better value along a → c and then c → d. In the topological order every edge into a vertex was relaxed before any edge out of it, so one pass of 6 relaxations did what the unlucky order did in 18.

Every algorithm in this module is a rule for choosing the order. Dijkstra’s algorithm, published in 1959, always relaxes the edges out of the closest vertex it has not finished. Bellman-Ford, from Richard Bellman’s 1958 paper, relaxes every edge in rounds until nothing changes. The DAG algorithm uses a topological order.

Negative edges and negative cycles

Weights can be negative: a cashback on a fare, a refund, energy recovered on a downhill stretch, a lead in a project plan. Negative edges are harmless in themselves. A negative cycle is not: a cycle whose weights add up to less than zero can be walked again and again, each lap making the total smaller.

Relaxation never settles when a negative cycle can be reached Python · negative/negative_cycle.py
# What a negative cycle does to "shortest". The cycle a -> b -> c -> a weighs 2 + (-4) + 1 = -1, so every lap
# around it lowers the cost of reaching a, b, c and t by 1, and relaxation never settles.
import math


def rounds(vertices, edges, source, count):
    """Relaxes every edge `count` times and prints the distances after each round."""
    dist = {v: math.inf for v in vertices}
    dist[source] = 0
    print("round " + "".join(f"{v:>6}" for v in vertices))
    for r in range(1, count + 1):
        changed = 0
        for u, v, w in edges:
            if dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                changed += 1
        row = "".join(f"{'inf' if d == math.inf else d:>6}" for d in dist.values())
        print(f"{r:>5} {row}   ({changed} changed)")


vertices = ["s", "a", "b", "c", "t"]
cycle = [("a", "b", 2), ("b", "c", -4), ("c", "a", 1)]
print("1. The cycle can be reached from s (edge s -> a):")
rounds(vertices, [("s", "a", 1), *cycle, ("c", "t", 3)], "s", 8)
print()
print("2. The same cycle, but s has no edge into it (s -> t only, and c -> t):")
rounds(vertices, [("s", "t", 5), *cycle, ("c", "t", 3)], "s", 3)

Output

1. The cycle can be reached from s (edge s -> a):
round      s     a     b     c     t
    1      0     0     3    -1     2   (5 changed)
    2      0    -1     2    -2     1   (4 changed)
    3      0    -2     1    -3     0   (4 changed)
    4      0    -3     0    -4    -1   (4 changed)
    5      0    -4    -1    -5    -2   (4 changed)
    6      0    -5    -2    -6    -3   (4 changed)
    7      0    -6    -3    -7    -4   (4 changed)
    8      0    -7    -4    -8    -5   (4 changed)

2. The same cycle, but s has no edge into it (s -> t only, and c -> t):
round      s     a     b     c     t
    1      0   inf   inf   inf     5   (1 changed)
    2      0   inf   inf   inf     5   (0 changed)
    3      0   inf   inf   inf     5   (0 changed)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 negative_cycle.py

In the first graph the cycle a → b → c → a weighs −1, and after round 1 every round lowered a, b, c and t by one more; there is no shortest walk to them, and their distances are −∞. In the second graph the same cycle sits where s cannot reach it, and the distance to t settled at 5 after one round. So distances are well defined exactly when no negative cycle lies on a walk from s to the vertex. Asking for the cheapest path that repeats no vertex does not rescue the first graph: with every weight set to −1 that path would be the one through the most vertices, so a fast method for it would also decide whether a graph has a Hamiltonian cycle, a problem Richard Karp proved NP-complete in 1972. The algorithms therefore report a reachable negative cycle instead of a distance, as the Bellman-Ford lesson shows.

Shortest paths in a DAG, in one pass

A DAG has a topological order, a listing of its vertices in which every edge goes from an earlier vertex to a later one; Kahn’s 1962 algorithm finds it by repeatedly taking a vertex with no edges left coming in. Every path visits its vertices in that order, so by the time the algorithm reaches a vertex u, every edge into u has already been relaxed and dist[u] is final. Relaxing u’s outgoing edges then passes final values on. Each vertex is taken once and each edge relaxed once, which is O(V + E), and nothing in the argument cares about the sign of the weights, because a DAG has no cycle at all, negative or not.

The van in this example works through a day of deliveries. A vertex is a stop at a time, so every edge points forward in time and the graph cannot have a cycle. An edge weighs the fuel for the drive minus the fee paid at the stop it reaches, so a drive that earns money weighs less than zero:

Shortest paths in a DAG with negative weights, in Python and JavaScript

Python · dag/dag_paths.py

# Shortest paths in a DAG: a topological order (Kahn's algorithm), then one relaxation of every edge in that order.
# O(V + E), and negative weights are fine. The graph is a delivery van's day (made up): a vertex is a stop at a time,
# so every edge goes forward in time and the graph has no cycle. An edge weighs the fuel to drive it minus the fee
# paid at the stop it reaches, so a negative weight is a drive that earns more than it costs.
import math
from collections import deque


def dag_shortest_paths(n, edges, source):
    """Returns (dist, parent) for vertices 0..n-1; raises ValueError if the graph has a cycle."""
    out = [[] for _ in range(n)]
    indegree = [0] * n
    for u, v, w in edges:
        out[u].append((v, w))
        indegree[v] += 1
    order = []
    ready = deque(v for v in range(n) if indegree[v] == 0)
    while ready:  # Kahn: take a vertex with no edge left coming in, then remove its edges
        u = ready.popleft()
        order.append(u)
        for v, _ in out[u]:
            indegree[v] -= 1
            if indegree[v] == 0:
                ready.append(v)
    if len(order) < n:
        raise ValueError("the graph has a cycle, so it has no topological order")
    dist = [math.inf] * n
    parent = [None] * n
    dist[source] = 0
    for u in order:
        if dist[u] == math.inf:
            continue  # not reachable from the source: nothing to relax
        for v, w in out[u]:
            if dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                parent[v] = u
    return dist, parent, order


def path_to(parent, v):
    path = [v]
    while parent[path[-1]] is not None:
        path.append(parent[path[-1]])
    return path[::-1]


stops = ["depot 8:00", "bakery 9:00", "school 10:00", "clinic 10:30", "market 12:00", "office 13:00", "depot 17:00"]
fee = [0, 0, 60, 90, 40, 70, 0]
drives = [(0, 1, 30), (0, 2, 50), (1, 2, 20), (1, 3, 35), (2, 4, 25), (2, 5, 45),
          (3, 4, 30), (3, 5, 40), (4, 5, 15), (4, 6, 30), (5, 6, 35)]
edges = [(u, v, fuel - fee[v]) for u, v, fuel in drives]
print("edge weights (fuel minus fee):", ", ".join(f"{u}->{v} {w}" for u, v, w in edges))
dist, parent, order = dag_shortest_paths(len(stops), edges, 0)
print("topological order:", ", ".join(stops[v] for v in order))
for v in range(1, len(stops)):
    route = " -> ".join(stops[x].split()[0] for x in path_to(parent, v))
    print(f"  {stops[v]:13} {dist[v]:4}   {route}")
print(f"best day: net cost {dist[6]}, so the van earns {-dist[6]} more than its fuel costs")

Output

edge weights (fuel minus fee): 0->1 30, 0->2 -10, 1->2 -40, 1->3 -55, 2->4 -15, 2->5 -25, 3->4 -10, 3->5 -30, 4->5 -55, 4->6 30, 5->6 35
topological order: depot 8:00, bakery 9:00, school 10:00, clinic 10:30, market 12:00, office 13:00, depot 17:00
  bakery 9:00     30   depot -> bakery
  school 10:00   -10   depot -> school
  clinic 10:30   -25   depot -> bakery -> clinic
  market 12:00   -35   depot -> bakery -> clinic -> market
  office 13:00   -90   depot -> bakery -> clinic -> market -> office
  depot 17:00    -55   depot -> bakery -> clinic -> market -> office -> depot
best day: net cost -55, so the van earns 55 more than its fuel costs

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 dag_paths.py

JavaScript · dag/dag_paths.mjs

// Shortest paths in a DAG: a topological order (Kahn's algorithm), then one relaxation of every edge in that order.
// O(V + E), and negative weights are fine. The same delivery van's day as dag_paths.py.
function dagShortestPaths(n, edges, source) {
  const out = Array.from({ length: n }, () => []);
  const indegree = new Array(n).fill(0);
  for (const [u, v, w] of edges) {
    out[u].push([v, w]);
    indegree[v] += 1;
  }
  const order = [];
  const ready = [];
  for (let v = 0; v < n; v++) if (indegree[v] === 0) ready.push(v);
  for (let head = 0; head < ready.length; head++) {
    // a queue with a head index: shift() would move every item each time
    const u = ready[head];
    order.push(u);
    for (const [v] of out[u]) if (--indegree[v] === 0) ready.push(v);
  }
  if (order.length < n) throw new Error('the graph has a cycle, so it has no topological order');
  const dist = new Array(n).fill(Infinity);
  const parent = new Array(n).fill(null);
  dist[source] = 0;
  for (const u of order) {
    if (dist[u] === Infinity) continue; // not reachable from the source: nothing to relax
    for (const [v, w] of out[u]) {
      if (dist[u] + w < dist[v]) {
        dist[v] = dist[u] + w;
        parent[v] = u;
      }
    }
  }
  return { dist, parent, order };
}

function pathTo(parent, v) {
  const path = [v];
  while (parent[path[path.length - 1]] !== null) path.push(parent[path[path.length - 1]]);
  return path.reverse();
}

const stops = ['depot 8:00', 'bakery 9:00', 'school 10:00', 'clinic 10:30', 'market 12:00', 'office 13:00', 'depot 17:00'];
const fee = [0, 0, 60, 90, 40, 70, 0];
const drives = [[0, 1, 30], [0, 2, 50], [1, 2, 20], [1, 3, 35], [2, 4, 25], [2, 5, 45],
  [3, 4, 30], [3, 5, 40], [4, 5, 15], [4, 6, 30], [5, 6, 35]];
const edges = drives.map(([u, v, fuel]) => [u, v, fuel - fee[v]]);
console.log('edge weights (fuel minus fee): ' + edges.map(([u, v, w]) => `${u}->${v} ${w}`).join(', '));
const { dist, parent, order } = dagShortestPaths(stops.length, edges, 0);
console.log('topological order: ' + order.map((v) => stops[v]).join(', '));
for (let v = 1; v < stops.length; v++) {
  const route = pathTo(parent, v).map((x) => stops[x].split(' ')[0]).join(' -> ');
  console.log(`  ${stops[v].padEnd(13)} ${String(dist[v]).padStart(4)}   ${route}`);
}
console.log(`best day: net cost ${dist[6]}, so the van earns ${-dist[6]} more than its fuel costs`);

Output

edge weights (fuel minus fee): 0->1 30, 0->2 -10, 1->2 -40, 1->3 -55, 2->4 -15, 2->5 -25, 3->4 -10, 3->5 -30, 4->5 -55, 4->6 30, 5->6 35
topological order: depot 8:00, bakery 9:00, school 10:00, clinic 10:30, market 12:00, office 13:00, depot 17:00
  bakery 9:00     30   depot -> bakery
  school 10:00   -10   depot -> school
  clinic 10:30   -25   depot -> bakery -> clinic
  market 12:00   -35   depot -> bakery -> clinic -> market
  office 13:00   -90   depot -> bakery -> clinic -> market -> office
  depot 17:00    -55   depot -> bakery -> clinic -> market -> office -> depot
best day: net cost -55, so the van earns 55 more than its fuel costs

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node dag_paths.mjs

The cheapest day goes through the bakery, the clinic, the market and the office, with a net cost of −55: the fees earned beat the fuel by 55. The school is a tie at −10, either directly from the depot or through the bakery. One line deserves attention: the loop skips a vertex whose distance is still infinite. Python’s math.inf plus −55 is still infinite, but code that stores ∞ as a large integer would turn “unreachable” into a real-looking distance after one negative edge, unless it skips such vertices.

Three orders of relaxation, counted

The order decides the work, not the answer. This run relaxes the edges of one random DAG, with 1,000 vertices, 6,000 edges and weights from −10 to 100, in three orders, and counts the relaxations:

One DAG, three orders of relaxation Python · orders/three_orders.py
# One random DAG, three orders of relaxing its edges. Each order ends with the same distances; the work differs.
import math
import random
from collections import deque

rng = random.Random(12)
V, E = 1_000, 6_000
rank = list(range(V))
rng.shuffle(rank)  # rank[v]: v's place in a hidden order; every edge goes from a lower rank to a higher one
by_rank = sorted(range(V), key=rank.__getitem__)
source = by_rank[0]
pairs = set()
for r in range(1, V):  # one edge into every vertex from an earlier one, so all of them can be reached
    pairs.add((by_rank[rng.randrange(r)], by_rank[r]))
while len(pairs) < E:
    a, b = sorted(rng.sample(range(V), 2))
    pairs.add((by_rank[a], by_rank[b]))
edges = [(u, v, rng.randint(-10, 100)) for u, v in sorted(pairs)]  # sorted: a set's order differs between builds
rng.shuffle(edges)


def topological_order():
    out = [[] for _ in range(V)]
    indegree = [0] * V
    for u, v, _ in edges:
        out[u].append(v)
        indegree[v] += 1
    ready = deque(v for v in range(V) if indegree[v] == 0)
    order = []
    while ready:
        u = ready.popleft()
        order.append(u)
        for v in out[u]:
            indegree[v] -= 1
            if indegree[v] == 0:
                ready.append(v)
    return order


def relax_in_passes(edge_order, max_passes):
    """Relaxes the edges in this order, pass after pass, until a pass lowers nothing (or max_passes)."""
    dist = [math.inf] * V
    dist[source] = 0
    passes = relaxed = 0
    while passes < max_passes:
        passes += 1
        changed = False
        for u, v, w in edge_order:
            relaxed += 1
            if dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                changed = True
        if not changed:
            break
    return dist, passes, relaxed


order = topological_order()
position = {v: i for i, v in enumerate(order)}
by_tail = sorted(edges, key=lambda e: position[e[0]])
runs = {
    "topological order, one pass": relax_in_passes(by_tail, 1),
    "input order, until a pass is quiet": relax_in_passes(edges, V),
    "reverse topological order, likewise": relax_in_passes(by_tail[::-1], V),
}
print(f"a random DAG: {V:,} vertices, {E:,} edges, weights from -10 to 100")
print(f"{'order of the relaxations':37} {'passes':>6} {'relaxations':>12}")
for name, (_, passes, relaxed) in runs.items():
    print(f"{name:37} {passes:>6} {relaxed:>12,}")
answers = [run[0] for run in runs.values()]
print(f"all three agree on every distance: {answers[0] == answers[1] == answers[2]}")

# Why the last order needs so many passes: each pass carries a distance only one edge further along a path.
# hops[v]: the fewest edges on any shortest path to v, filled in topological order.
dist = answers[0]
hops = [math.inf] * V
hops[source] = 0
for u, v, w in by_tail:
    if dist[u] + w == dist[v]:
        hops[v] = min(hops[v], hops[u] + 1)
print(f"some vertex needs {max(hops)} edges on its shortest path: {max(hops)} passes, then one quiet pass")

Output

a random DAG: 1,000 vertices, 6,000 edges, weights from -10 to 100
order of the relaxations              passes  relaxations
topological order, one pass                1        6,000
input order, until a pass is quiet         7       42,000
reverse topological order, likewise       10       60,000
all three agree on every distance: True
some vertex needs 9 edges on its shortest path: 9 passes, then one quiet pass

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 three_orders.py

All three orders agree on every distance. The topological order relaxed each edge once, 6,000 relaxations. Repeating passes over the edges in the order they were stored needed 7 passes, the last of them only to see that nothing changed: 42,000 relaxations. The reverse topological order is the worst order there is. Each pass carries a distance just one edge further along every path, so it needed one pass per edge of the longest shortest path, 9, plus the quiet one: 60,000. The second and third orders are Bellman-Ford’s rounds, which work on any graph but pay for not knowing the order; the DAG algorithm knows it.

Longest paths and the critical path

For general graphs the longest path that repeats no vertex is as hard as the Hamiltonian cycle problem. In a DAG it is easy: negate every weight and find the shortest path, or, equivalently, relax with > instead of <. That is the critical-path method of project planning, described by James Kelley and Morgan Walker in 1959. Each task can start when all its prerequisites have finished, so its earliest start is the longest path to it, and the length of the whole project is the longest path through the plan.

A wedding plan of eleven tasks as a DAG; the critical path runs through the outfits and their 15 days of tailoring.fix the date2 daysvenue7guest list5outfits4caterer4decorate2print cards6tailoring15send cards3mehendi, sangeet2wedding day1 (starts on day 23)

The plan as a DAG, with its critical path

Text description of the diagram

Each box is a task with its duration in days; an arrow runs from a task to a task that cannot start before it ends.

  1. Fixing the date (2 days) comes first. Three tasks wait for it: booking the venue (7), drawing up the guest list (5) and choosing the outfits (4).
  2. Booking the caterer (4), decorating (2) and printing the invitations (6) wait for the venue; printing also waits for the guest list. Sending the invitations (3) follows printing.
  3. Tailoring and fittings (15 days) follow the choice of outfits.
  4. The mehendi and sangeet (2) wait for both the tailoring and the invitations. The wedding day (1) waits for the mehendi and sangeet, the caterer and the decoration.
  5. The thick arrows mark the critical path: fix the date, choose the outfits, tailoring and fittings, mehendi and sangeet, wedding day. Its 2 + 4 + 15 + 2 + 1 = 24 days are the longest path through the plan, so the wedding day starts on day 23 and everything ends after 24 days.
Earliest and latest starts of a wedding plan, with its critical path Python · wedding/critical_path.py
# The critical path of a wedding plan (tasks and durations made up). Each task can start only when its prerequisites
# are done, so the earliest start of a task is the LONGEST path to it: relaxation with max instead of min, in
# topological order. graphlib.TopologicalSorter (standard library) supplies the order and refuses a cycle.
from graphlib import TopologicalSorter

tasks = {  # task: (days, prerequisites)
    "fix the date": (2, []),
    "book the venue": (7, ["fix the date"]),
    "draw up the guest list": (5, ["fix the date"]),
    "book the caterer": (4, ["book the venue"]),
    "print the invitations": (6, ["book the venue", "draw up the guest list"]),
    "send the invitations": (3, ["print the invitations"]),
    "choose the outfits": (4, ["fix the date"]),
    "tailoring and fittings": (15, ["choose the outfits"]),
    "decorate the venue": (2, ["book the venue"]),
    "mehendi and sangeet": (2, ["tailoring and fittings", "send the invitations"]),
    "wedding day": (1, ["mehendi and sangeet", "book the caterer", "decorate the venue"]),
}
days = {t: d for t, (d, _) in tasks.items()}
after = {t: [] for t in tasks}
for t, (_, before) in tasks.items():
    for p in before:
        after[p].append(t)
order = list(TopologicalSorter({t: before for t, (_, before) in tasks.items()}).static_order())

# Forward pass: relax each edge t -> s with "can s start later?", the longest-path version of relaxation.
start = {t: 0 for t in tasks}
waits_for = {t: None for t in tasks}
for t in order:
    for s in after[t]:
        if start[t] + days[t] > start[s]:
            start[s] = start[t] + days[t]
            waits_for[s] = t
finish = max(start[t] + days[t] for t in tasks)

# Backward pass, in reverse topological order: the latest start that still finishes everything on time.
latest = {}
for t in reversed(order):
    latest[t] = min([latest[s] for s in after[t]], default=finish) - days[t]

print(f"{'task':24} {'days':>4} {'earliest':>9} {'latest':>7} {'slack':>6}")
for t in order:
    slack = latest[t] - start[t]
    print(f"{t:24} {days[t]:>4} {start[t]:>9} {latest[t]:>7} {slack:>6}{'  critical' if slack == 0 else ''}")
path = ["wedding day"]
while waits_for[path[-1]] is not None:
    path.append(waits_for[path[-1]])
print(f"everything is done after {finish} days, the length of the longest path")
print("critical path:", " -> ".join(reversed(path)))

Output

task                     days  earliest  latest  slack
fix the date                2         0       0      0  critical
book the venue              7         2       5      3
draw up the guest list      5         2       7      5
choose the outfits          4         2       2      0  critical
book the caterer            4         9      19     10
decorate the venue          2         9      21     12
print the invitations       6         9      12      3
tailoring and fittings     15         6       6      0  critical
send the invitations        3        15      18      3
mehendi and sangeet         2        21      21      0  critical
wedding day                 1        23      23      0  critical
everything is done after 24 days, the length of the longest path
critical path: fix the date -> choose the outfits -> tailoring and fittings -> mehendi and sangeet -> wedding day

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 critical_path.py

The forward pass in topological order gives each task’s earliest start. A backward pass in reverse order gives the latest start that still lets everything finish on day 24, and the difference is the task’s slack. The tasks with no slack form the critical path: the date, the outfits, 15 days of tailoring, the mehendi and sangeet, and the wedding day. The venue, which feels like the big decision, has 3 days of slack, and the caterer has 10. Finishing the venue early gains nothing; one more day of tailoring moves the wedding by one day. Python’s graphlib.TopologicalSorter supplied the order and would have raised CycleError if two tasks waited for each other.

Choosing a shortest-path algorithm

The weights and the shape of the graph decide the algorithm. The first two rows belong to the graph-traversal module; the lessons after this one cover the others:

The graph Algorithm Time
All weights equal BFS O(V + E)
Weights 0 or 1 0-1 BFS O(V + E)
A DAG, any weights Topological order O(V + E)
No weight below 0 Dijkstra O((V + E) log V)
Some weights below 0 Bellman-Ford O(VE)
Every pair, dense Floyd-Warshall O(V³)
Every pair, sparse Johnson O(VE log V)

Exercise: the critical path of a project

A project plan gives each pair of milestones a minimum time between their starts, some of them negative, and asks for the earliest start of the last milestone. The tests check small plans against every path and end with a plan of 100,000 milestones.

Exercise · Medium · Python, JavaScript

The longest path between two milestones of a project

A project plan is a directed acyclic graph on the milestones 0 to n - 1. An edge (u, v, w) (JavaScript: a three-element array) says that v can start no earlier than w days after u starts. w may be negative: then v may start before u (a lead). Write longest_path(n, edges, s, t) (JavaScript: longestPath(n, edges, s, t)), which returns the earliest day on which t can start when s starts on day 0, that is, the largest total weight of a path from s to t, together with one path that has it, as a list of milestones from s to t.

Python returns a tuple (length, path), JavaScript an array [length, path]. When no path leads from s to t, return None (JavaScript: null). When s and t are the same milestone, the answer is (0, [s]).

The edges come in any order, and the numbers of the milestones say nothing about their order in the project. The tests include negative edges that a longest path still uses, milestones that cannot be reached, 300 small random projects checked against every path, and a project of 100,000 milestones and about 200,000 edges whose longest path runs through every milestone: trying every path, or recursing once per milestone, does not finish.

Python · Starter code · critical.py

def longest_path(n, edges, s, t):
    """DAG on milestones 0..n-1, edges (u, v, w). Returns (length, path) of a longest s-t path, or None."""
    raise NotImplementedError("write longest_path")
The sample tests · test_critical.py
import random

from critical import longest_path


def best_by_every_path(n, edges, s, t):
    """The slow answer: the largest weight over every path from s to t (None if there is none)."""
    out = [[] for _ in range(n)]
    for u, v, w in edges:
        out[u].append((v, w))
    best = None
    stack = [(s, 0)]
    while stack:
        u, total = stack.pop()
        if u == t and (best is None or total > best):
            best = total
        for v, w in out[u]:
            stack.append((v, total + w))
    return best


def check_path(edges, s, t, length, path):
    """The path starts at s, ends at t, uses only edges of the graph and weighs exactly `length`."""
    weight = {}
    for u, v, w in edges:
        weight[(u, v)] = max(w, weight.get((u, v), w))
    assert path[0] == s and path[-1] == t, path
    assert all(step in weight for step in zip(path, path[1:])), path
    assert sum(weight[step] for step in zip(path, path[1:])) == length, path


def test_small_project():
    """two routes to the finish: 0, 2, 3, 4, 5 weighs 14 and beats 0, 1, 3, 4, 5 with 13"""
    edges = [(0, 1, 3), (0, 2, 2), (1, 3, 4), (2, 3, 6), (3, 4, 1), (2, 4, 2), (4, 5, 5)]
    assert longest_path(6, edges, 0, 5) == (14, [0, 2, 3, 4, 5])


def test_negative_edge_on_the_longest_path():
    """a lead of -2 days still lies on the longest path, because the edge after it is long"""
    edges = [(0, 1, -2), (1, 3, 10), (0, 2, 4), (2, 3, 3)]
    assert longest_path(4, edges, 0, 3) == (8, [0, 1, 3])


def test_labels_are_not_the_order():
    """milestone 4 comes first and 0 last"""
    edges = [(4, 2, 1), (2, 0, 7), (4, 3, 2), (3, 0, 1), (2, 3, 5)]
    assert longest_path(5, edges, 4, 0) == (8, [4, 2, 0])


def test_unreachable_target():
    """no path leads from 0 to 2"""
    assert longest_path(3, [(1, 2, 5)], 0, 2) is None


def test_source_is_target():
    """the answer for s = t is a path of one milestone"""
    assert longest_path(3, [(0, 1, 4)], 0, 0) == (0, [0])


def test_random_projects_against_every_path():
    """300 small random projects: the length matches every path tried, and the path is a real one"""
    rng = random.Random(11)
    for _ in range(300):
        n = rng.randint(1, 7)
        hidden = list(range(n))
        rng.shuffle(hidden)
        pairs = {(hidden[i], hidden[j]) for i in range(n) for j in range(i + 1, n) if rng.random() < 0.45}
        edges = [(u, v, rng.randint(-5, 9)) for u, v in sorted(pairs)]
        rng.shuffle(edges)
        s, t = rng.randrange(n), rng.randrange(n)
        best = best_by_every_path(n, edges, s, t)
        result = longest_path(n, edges, s, t)
        if best is None:
            assert result is None, (n, edges, s, t)
        else:
            assert result is not None and result[0] == best, (n, edges, s, t, result)
            check_path(edges, s, t, *result)


def test_large_project():
    """100,000 milestones in a hidden order and about 200,000 edges: the chain through all of them is longest"""
    rng = random.Random(21)
    n = 100_000
    order = list(range(n))
    rng.shuffle(order)
    steps = [rng.randint(-3, 10) for _ in range(n - 1)]
    prefix = [0]
    for w in steps:
        prefix.append(prefix[-1] + w)
    edges = [(order[i], order[i + 1], steps[i]) for i in range(n - 1)]
    for _ in range(100_000):  # shortcuts that skip milestones, each a little shorter than the chain it skips
        i, j = rng.randrange(n), rng.randrange(n)
        if i > j:
            i, j = j, i
        if j > i + 1:
            edges.append((order[i], order[j], prefix[j] - prefix[i] - rng.randint(1, 20)))
    rng.shuffle(edges)
    length, path = longest_path(n, edges, order[0], order[-1])
    assert length == prefix[-1]
    assert path == order

JavaScript · Starter code · critical.mjs

/** DAG on milestones 0..n-1, edges [u, v, w]. Returns [length, path] of a longest s-t path, or null. */
export function longestPath(n, edges, s, t) {
  throw new Error('write longestPath');
}
The sample tests · critical.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { longestPath } from './critical.mjs';

function bestByEveryPath(n, edges, s, t) {
  const out = Array.from({ length: n }, () => []);
  for (const [u, v, w] of edges) out[u].push([v, w]);
  let best = null;
  const stack = [[s, 0]];
  while (stack.length) {
    const [u, total] = stack.pop();
    if (u === t && (best === null || total > best)) best = total;
    for (const [v, w] of out[u]) stack.push([v, total + w]);
  }
  return best;
}

function checkPath(edges, s, t, length, path) {
  const weight = new Map();
  for (const [u, v, w] of edges) weight.set(`${u},${v}`, Math.max(w, weight.get(`${u},${v}`) ?? w));
  assert.ok(path[0] === s && path[path.length - 1] === t, JSON.stringify(path));
  let total = 0;
  for (let i = 0; i + 1 < path.length; i++) {
    const key = `${path[i]},${path[i + 1]}`;
    assert.ok(weight.has(key), `no edge ${key} in the graph`);
    total += weight.get(key);
  }
  assert.equal(total, length, JSON.stringify(path));
}

function seeded(seed) {
  return () => (seed = (seed * 48271) % 2147483647) / 2147483647;
}
const below = (random, k) => Math.floor(random() * k);
function shuffle(random, a) {
  for (let i = a.length - 1; i > 0; i--) {
    const j = below(random, i + 1);
    [a[i], a[j]] = [a[j], a[i]];
  }
  return a;
}

test('two routes to the finish: 0, 2, 3, 4, 5 weighs 14 and beats 0, 1, 3, 4, 5 with 13', () => {
  const edges = [[0, 1, 3], [0, 2, 2], [1, 3, 4], [2, 3, 6], [3, 4, 1], [2, 4, 2], [4, 5, 5]];
  assert.deepEqual(longestPath(6, edges, 0, 5), [14, [0, 2, 3, 4, 5]]);
});

test('a lead of -2 days still lies on the longest path, because the edge after it is long', () => {
  const edges = [[0, 1, -2], [1, 3, 10], [0, 2, 4], [2, 3, 3]];
  assert.deepEqual(longestPath(4, edges, 0, 3), [8, [0, 1, 3]]);
});

test('milestone 4 comes first and 0 last', () => {
  const edges = [[4, 2, 1], [2, 0, 7], [4, 3, 2], [3, 0, 1], [2, 3, 5]];
  assert.deepEqual(longestPath(5, edges, 4, 0), [8, [4, 2, 0]]);
});

test('no path leads from 0 to 2', () => {
  assert.equal(longestPath(3, [[1, 2, 5]], 0, 2), null);
});

test('the answer for s = t is a path of one milestone', () => {
  assert.deepEqual(longestPath(3, [[0, 1, 4]], 0, 0), [0, [0]]);
});

test('300 small random projects: the length matches every path tried, and the path is a real one', () => {
  const random = seeded(11);
  for (let k = 0; k < 300; k++) {
    const n = 1 + below(random, 7);
    const hidden = shuffle(random, Array.from({ length: n }, (_, i) => i));
    const edges = [];
    for (let i = 0; i < n; i++) for (let j = i + 1; j < n; j++) if (random() < 0.45) edges.push([hidden[i], hidden[j], below(random, 15) - 5]);
    shuffle(random, edges);
    const s = below(random, n);
    const t = below(random, n);
    const best = bestByEveryPath(n, edges, s, t);
    const result = longestPath(n, edges, s, t);
    const where = JSON.stringify([n, edges, s, t]);
    if (best === null) assert.equal(result, null, where);
    else {
      assert.ok(result !== null && result[0] === best, where);
      checkPath(edges, s, t, result[0], result[1]);
    }
  }
});

test('100,000 milestones in a hidden order and about 200,000 edges: the chain through all of them is longest', () => {
  const random = seeded(21);
  const n = 100000;
  const order = shuffle(random, Array.from({ length: n }, (_, i) => i));
  const prefix = [0];
  const edges = [];
  for (let i = 0; i + 1 < n; i++) {
    const w = below(random, 14) - 3;
    prefix.push(prefix[i] + w);
    edges.push([order[i], order[i + 1], w]);
  }
  for (let k = 0; k < 100000; k++) {
    let i = below(random, n);
    let j = below(random, n);
    if (i > j) [i, j] = [j, i];
    if (j > i + 1) edges.push([order[i], order[j], prefix[j] - prefix[i] - 1 - below(random, 20)]);
  }
  shuffle(random, edges);
  const [length, path] = longestPath(n, edges, order[0], order[n - 1]);
  assert.equal(length, prefix[n - 1]);
  assert.deepEqual(path, order);
});
A hint

Find a topological order first, with Kahn's algorithm and a queue rather than recursion, because the largest test is a chain 100,000 milestones long. Then relax every edge in that order with "longer" instead of "shorter": start every milestone at minus infinity except s at 0, skip milestones still at minus infinity, and keep the edge that last raised each milestone, so the path can be read back from t.

The sample tests run on this device, in your browser (Pyodide, QuickJS): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Gantt Chart Maker Enter the wedding tasks with the earliest starts from the table and see the critical path as the chain of bars with no gap between them.

Check yourself

5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 5 dist[u] is 7, dist[v] is 12 and the edge u → v weighs 4. What is dist[v] after relaxing that edge?

    Type a number.

    Show the answer to question 1

    Answer: 11

    7 + 4 = 11 is below 12, so the route through u is cheaper: dist[v] becomes 11 and u becomes v's parent. Had the edge weighed 6, the total 13 would not be below 12 and nothing would change.

  2. Question 2 of 5 What does breadth-first search return when it is run on a graph with different edge weights?

    Choose one answer.

    Show the answer to question 2

    Answer: A path with the fewest edges, which may weigh more than the shortest path

    BFS visits vertices in order of the number of edges from the source. That is the order of distance only when every edge weighs the same; on the rider's map it chose one road of 9 minutes over three roads of 7 minutes in all.

  3. Question 3 of 5 A graph has a cycle whose weights add up to −3. When are the distances from s still well defined?

    Choose one answer.

    Show the answer to question 3

    Answer: For every vertex that no walk from s can reach through that cycle

    The cycle lowers a total only on walks that can pass through it. If s cannot reach the cycle, or the cycle cannot reach v, no walk from s to v can use it, and dist[v] settles as usual; the vertices the cycle does affect have distance −∞.

  4. Question 4 of 5 In the wedding plan, tailoring takes 18 days instead of 15. After how many days is everything done?

    Type a number.

    Show the answer to question 4

    Answer: 27 days

    Tailoring is on the critical path, so every extra day of it moves the end by a day: 24 + 3 = 27. The new longest path is still the date, the outfits, the tailoring, the mehendi and sangeet and the wedding day.

  5. Question 5 of 5 In the wedding plan, printing the invitations takes 8 days instead of 6. What happens to the wedding?

    Choose one answer.

    Show the answer to question 5

    Answer: Nothing; the invitations had 3 days of slack, so 1 is left

    Printing had 3 days of slack: it could start on day 12 instead of day 9 without delaying anything. Two extra days use 2 of those 3, so the end stays on day 24. Only tasks with no slack move the end date.

Interview questions

What does it mean to relax an edge, and what do Dijkstra’s algorithm, Bellman-Ford and the DAG algorithm have in common? Relaxing the edge u → v of weight w checks whether the best known route to u, followed by that edge, is cheaper than the best known route to v; if it is, dist[v] becomes dist[u] + w and u becomes v’s parent. The estimate is always the weight of some real walk, so it never falls below the true distance, and relaxing the edges of a shortest path in order makes it exact. All three algorithms start from dist[s] = 0 and infinity elsewhere and do nothing but relax edges. They differ only in the order: Dijkstra relaxes the edges out of the closest unfinished vertex and needs non-negative weights, Bellman-Ford relaxes every edge in up to V − 1 rounds, and the DAG algorithm relaxes each edge once in topological order.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A monorepo has 40,000 build targets and 150,000 "needs" edges between them, and you know how long each target takes to build. With as many machines as you like, how long does a clean build take, and which targets should the team speed up first? Explain the computation and its cost.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    Every road segment in a city map takes 1, 2 or 3 minutes, and a teammate proposes plain breadth-first search for the fastest route because the weights are almost the same. When does that give a wrong answer, and what would you use for a map of 10⁶ segments?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A shortest path minimises the sum of the edge weights; a solution returns dist and parent, and the parents form a shortest-path tree.
  • BFS minimises the number of edges, which is the right answer only when all weights are equal.
  • Relaxation keeps dist[v] an upper bound that only goes down; relaxing a shortest path’s edges in order makes it exact, and the algorithms differ only in the order they relax.
  • A negative cycle on a walk from s makes the distances it reaches −∞; negative edges alone are fine.
  • In a DAG, relaxing in topological order solves the problem in O(V + E) with any weights, and relaxing with > gives the longest path, the critical path of a plan.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.