Pattern 12 of 20

Advanced Graphs

Weighted shortest paths, minimum spanning trees, topological order and union-find.

When to use it

  • Edges have different costs and you need the cheapest route
  • Connecting every node at minimum total cost
  • Tasks with prerequisites, or merging groups as edges arrive

The idea

Pick the algorithm that matches the constraint: Dijkstra for non-negative weights, Bellman-Ford when the number of edges in a path is limited, Kruskal or Prim for spanning trees, Kahn's algorithm for ordering.

Typical cost Typically O(E log V) time.

How it works

Once edges have different costs, BFS no longer gives shortest paths, because a path with more edges can be cheaper. Dijkstra's algorithm fixes this by always expanding the closest unfinished node, using a min-heap keyed by distance.

Dijkstra works because with non-negative weights, the closest node in the heap can never be improved later: any other route goes through a node that is already at least as far. That is also why negative edges break it, and why Bellman-Ford (relax every edge V - 1 times) exists.

Minimum spanning trees connect every node at the lowest total cost. Prim's grows one tree from a start node with a heap of crossing edges; Kruskal's sorts all edges and adds each one that joins two different components, using union-find.

Disguises include network delay times, cheapest flights within k stops (Bellman-Ford with a stop limit), paths that minimize the maximum effort (Dijkstra on a different cost function), connecting points at minimum cost, and reconstructing an itinerary that uses every ticket (an Eulerian path).

How to attack it

  1. 1Check edge weights: all equal means BFS, non-negative means Dijkstra, negative means Bellman-Ford.
  2. 2Define what 'distance' means; it may be a maximum along the path, not a sum.
  3. 3Store (distance, node) in the heap and skip stale entries when popped.
  4. 4For 'connect everything cheaply', use Kruskal or Prim instead of shortest paths.
  5. 5Add any extra constraint (stops, fuel) to the state, not to the graph.

Template

A skeleton to adapt, not a solution to paste. Write it from memory a few times.

import heapq


def dijkstra(n, edges, source):
    graph = [[] for _ in range(n)]
    for u, v, w in edges:          # directed edge u -> v with weight w >= 0
        graph[u].append((v, w))
    dist = [float("inf")] * n
    dist[source] = 0
    heap = [(0, source)]
    while heap:
        d, u = heapq.heappop(heap)
        if d > dist[u]:
            continue               # stale entry: a shorter path was found
        for v, w in graph[u]:
            nd = d + w
            if nd < dist[v]:
                dist[v] = nd
                heapq.heappush(heap, (nd, v))
    return dist


def kruskal(n, edges):
    parent = list(range(n))
    def find(x):
        while parent[x] != x:
            parent[x] = parent[parent[x]]   # path halving
            x = parent[x]
        return x
    total = 0
    for w, u, v in sorted(edges):           # edges as (weight, u, v)
        ru, rv = find(u), find(v)
        if ru != rv:
            parent[ru], total = rv, total + w
    return total

Variants

Dijkstra
Single-source shortest paths with non-negative weights. Change the combine step to max or min for bottleneck paths.
Bellman-Ford
Relax all edges up to V - 1 times; handles negative weights, detects negative cycles, and limits path length to k edges naturally.
0-1 BFS
When weights are only 0 or 1, a deque replaces the heap: push weight-0 edges to the front and weight-1 edges to the back.
Minimum spanning tree
Kruskal sorts edges and uses union-find; Prim grows from one node with a heap. Both give the same total.
Eulerian path
Use every edge exactly once with Hierholzer's algorithm: DFS, and append a node only after its edges are exhausted.

Pitfalls

  • Running Dijkstra with negative edge weights.
  • Not skipping stale heap entries, which repeats work and can blow up the running time.
  • Marking a node final when it is pushed rather than when it is popped.
  • Encoding a constraint like 'at most k stops' outside the state, so the algorithm discards valid paths.
  • Confusing a minimum spanning tree with shortest paths from one source; they answer different questions.

What it costs

OperationCost
Dijkstra with a binary heapO((V + E) log V)
Bellman-FordO(V · E)
KruskalO(E log E) for the sort
Prim with a heapO(E log V)
Floyd-Warshall (all pairs)O(V³) time, O(V²) space

30 Advanced Graphs problems

Easiest first. Problems marked Free can be solved without Pro.

  1. 1Depot Delivery DistancesFreeEasy
  2. 2Cheapest Wiring PlanFreeEasy
  3. 3All-Pairs Travel TableFreeEasy
  4. 4One-Stroke DrawingEasy
  5. 5Net Cost With RefundsEasy
  6. 6Towns Within Fuel BudgetEasy
  7. 7Endless Discount LoopEasy
  8. 8Dense Power GridEasy
  9. 9Module Dependency QueriesEasy
  10. 10Gentlest Hiking RouteMedium
  11. 11Cheapest Route With LayoversMedium
  12. 12Link the OutpostsMedium
  13. 13Rebuild the Travel LogMedium
  14. 14Conveyor Grid FixesMedium
  15. 15Fragile Network LinksMedium
  16. 16Single Points of FailureMedium
  17. 17Quietest Hub TownMedium
  18. 18Most Reliable Relay PathMedium
  19. 19Count Fastest RoutesMedium
  20. 20Fewest Walls to BreakMedium
  21. 21Cheapest Water SupplyMedium
  22. 22Currency Loop ProfitMedium
  23. 23Timetable Earliest ArrivalMedium
  24. 24Robust Network ZonesMedium
  25. 25Toll Route With PassesHard
  26. 26Essential and Optional CablesHard
  27. 27Domino Chain OrderHard
  28. 28Cheapest Trip Under DeadlineHard
  29. 29Fortify the NetworkHard
  30. 30Fewest Turns MazeHard

Solve them in the browser, in Python or JavaScript.