E ExamMaster

CS Core & Software Engineering · Data Structures & Algorithms

Advanced Graph Algorithms

Shortest path, minimum spanning tree and topological sort algorithms on weighted graphs.

Seven concepts on weighted shortest paths, minimum spanning trees, topological order on DAGs, and all-pairs distances — the algorithms that sit on top of the BFS/DFS foundations in Trees and Graphs.

  • CS Core & Software Engineering
  • Hard level
  • 7 concepts
  • 5 practice questions

1Dijkstra: non-negative shortest paths

Dijkstra finds single-source shortest paths when every edge weight is non-negative. A min-priority queue repeatedly extracts the unsettled vertex with the smallest tentative distance; its distance is then final, and each outgoing edge is relaxed. With a binary heap the cost is O((V + E) \log V). A single negative edge can make a finalized distance wrong — that is why the algorithm refuses negatives.

Figure. Same digraph as the worked example. Node labels show final Dijkstra distances from A; edge labels are weights.

Relax from the nearest unsettled vertex

  1. SeedSet dist[s] = 0 and all other distances to \infty; push s into a min-heap keyed by distance.
  2. Extract-minPop the unsettled vertex u with smallest dist[u]. That value is now final for non-negative weights.
  3. RelaxFor each edge u → v of weight w, if dist[u] + w < dist[v], update dist[v] and push the new key.

Dijkstra with a binary heap

import heapq

def dijkstra(graph, s):
    dist = {s: 0}
    heap = [(0, s)]
    done = set()
    while heap:
        d, u = heapq.heappop(heap)
        if u in done:
            continue
        done.add(u)
        for v, w in graph[u]:
            nd = d + w
            if v not in dist or nd < dist[v]:
                dist[v] = nd
                heapq.heappush(heap, (nd, v))
    return dist

Shortest Path with Dijkstra

Directed edges A→B weight 4, A→C weight 2, C→B weight 1, B→D weight 5, C→D weight 8. Compute shortest distances from A.

  • seedA=0; heap [(0,A)]
  • pop A; relax B←4, C←2B=4, C=2
  • pop C (2); relax B←2+1, D←2+8B=3, D=10
  • pop B (3); relax D←3+5D=8
  • pop D (8); done{A:0, B:3, C:2, D:8}

Pro tip. Dijkstra is only valid with non-negative weights; a single negative edge can make a finalized distance wrong. Path A→C→B→D has length 2+1+5 = 8, which beats A→B→D = 9 and A→C→D = 10.

Coding lab. Dijkstra on the four-city digraph runs in the app, with checks on your output.

Dijkstra is unsafe when the graph has
  1. High degree vertices — the heap cannot store them
  2. A negative-weight edge — a finalized distance can still improve later
  3. Undirected edges — only directed graphs are allowed

Extract-min finalizes a vertex under the assumption that no later path can undercut it. A negative edge elsewhere can create a cheaper path after that finalization. Directionality and degree do not break the proof; negative weights do.

2Bellman-Ford: negatives and cycles

Bellman-Ford also computes single-source shortest paths, but it allows negative edge weights. It relaxes every edge in the graph for V - 1 rounds — enough for a shortest path of at most V - 1 edges to propagate. One extra round that still improves a distance proves a negative cycle is reachable from the source. Time is O(VE).

Figure. Same digraph as the ledger. The negative B→C edge is what forces Bellman-Ford over Dijkstra; node labels are final distances from A.

Relax all edges, then check

  1. Initializedist[s] = 0; every other distance \infty.
  2. V−1 roundsFor each of V - 1 passes, try every edge u → v: if dist[u] + w < dist[v], update dist[v].
  3. Cycle checkOne more pass over all edges; any further improvement means a negative cycle is reachable.

Bellman-Ford with cycle flag

def bellman_ford(n, edges, s):
    dist = [float('inf')] * n
    dist[s] = 0
    for _ in range(n - 1):
        for u, v, w in edges:
            if dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
    for u, v, w in edges:
        if dist[u] + w < dist[v]:
            return None  # negative cycle reachable
    return dist

Negatives that Dijkstra cannot trust

Directed edges A→B weight 4, A→C weight 5, B→C weight −3, C→D weight 2. Shortest distances from A (no negative cycle).

  • seedA=0; B,C,D = ∞
  • relax A→B, A→CB=4, C=5
  • relax B→C (−3)C=4+(−3)=1
  • relax C→DD=1+2=3
  • further rounds; no change; V-th pass quiet{A:0, B:4, C:1, D:3}; no neg. cycle

Pro tip. Non-negative weights ⇒ Dijkstra; negative weights or cycle detection needed ⇒ Bellman-Ford. Path A→B→C→D = 4−3+2 = 3 beats A→C→D = 7.

After V - 1 Bellman-Ford rounds, one more improving relaxation means
  1. The graph is denser than V edges, so another round is routine
  2. A negative cycle is reachable from the source
  3. Dijkstra should have been used instead

A simple shortest path has at most V-1 edges. Improvement after that many rounds cannot come from a simple path — some vertex is on a negative cycle reachable from s.

3Which shortest-path algorithm?

Pick the algorithm from the weight regime and the query shape. Non-negative single-source work is Dijkstra. Negatives or an explicit negative-cycle test need Bellman-Ford. Dense all-pairs queries favour Floyd-Warshall's O(V^3) DP over launching Dijkstra from every vertex when that constant-factor and coding cost lose.

Figure. Pick by weight regime: BFS for unit edges, Dijkstra for non-negative, Bellman-Ford when negatives appear.

Shortest-path picker
SituationAlgorithmWhy
Single source, all weights ≥ 0DijkstraO((V+E)\log V) heap; distances finalize on extract
Single source, negatives allowedBellman-FordO(VE); extra pass detects neg. cycles
All pairs, dense / simple codeFloyd-WarshallO(V^3) over intermediate vertices
Unweighted (every edge cost 1)BFSFirst reach is minimum hops — see Trees and Graphs
You need distances from one source on a graph that may contain a negative edge but no negative cycle. Prefer
  1. Dijkstra — extract-min still finalizes correctly
  2. Bellman-Ford — negatives are allowed; run the V-th pass to confirm no cycle
  3. BFS — hop count ignores weights

Dijkstra's finalization proof needs non-negative weights. Bellman-Ford handles negatives and the extra pass confirms the no-cycle assumption. BFS answers hop distance, not weighted length.

4Kruskal: sort edges, skip cycles

A minimum spanning tree of a connected undirected weighted graph is a subset of edges that links every vertex, forms no cycle, and has minimum total weight. Kruskal sorts all edges by increasing weight and adds an edge when its endpoints lie in different components — union-find answers that test. Sorting dominates: O(E \log E).

Figure. Same undirected graph as the Kruskal ledger. Solid edges AB, BC, CD form the MST; dashed AC and BD were skipped as cycles.

Grow a forest by lightest safe edge

  1. SortOrder every edge by non-decreasing weight.
  2. TestFor the next edge u–v, find the components of u and v (union-find).
  3. Add or skipIf the components differ, union them and keep the edge; if they match, the edge would close a cycle — skip it.

Kruskal with union-find

def kruskal(n, edges):
    parent = list(range(n))

    def find(x):
        while parent[x] != x:
            parent[x] = parent[parent[x]]
            x = parent[x]
        return x

    mst, total = [], 0
    for u, v, w in sorted(edges, key=lambda e: e[2]):
        ru, rv = find(u), find(v)
        if ru == rv:
            continue
        parent[rv] = ru
        mst.append((u, v, w))
        total += w
    return mst, total

MST by lightest edges

Undirected edges A–B weight 1, B–C weight 2, C–D weight 3, A–C weight 4, B–D weight 5. Build an MST with Kruskal.

  • sortAB1, BC2, CD3, AC4, BD5
  • add AB (1); components {A,B}{C}{D}weight 1
  • add BC (2); merge Cweight 1+2=3
  • add CD (3); merge Dweight 3+3=6; all linked
  • skip AC (4), BD (5) — same componentMST weight 6

Pro tip. Choose Kruskal for sparse graphs (edge-sorted with union-find); Prim is natural when you grow from a start vertex with a heap on dense adjacency.

Kruskal rejects an edge during the scan when
  1. Its weight is larger than the average edge weight
  2. Its endpoints are already in the same union-find component
  3. The heap is empty

Same component means the edge would close a cycle in the forest built so far. Kruskal never uses a heap; weight order comes from the initial sort.

5Prim: grow one tree from a root

Prim builds the same MST by growing a single tree from a start vertex. A heap stores the lightest edge from the tree to each outsider; extract-min pulls the next vertex into the tree and relaxes its edges to remaining outsiders. With a binary heap the cost is O(E \log V). On a dense graph Prim's adjacency-driven growth is the natural fit; Kruskal's global edge sort is the sparse-graph habit.

Same undirected four-vertex graph as Kruskal: sort edges, skip cycles — Prim grows one tree from a root by always adding the lightest edge out of the current set, ending at the same weight-6 MST.

Expand the cut by the lightest crossing edge

  1. StartPut an arbitrary root in the tree S; key[root] = 0 and other keys \infty.
  2. ExtractPop the outsider u with smallest key[u] — the lightest edge into S — and add u to S.
  3. Relax cutFor each edge u–v with v outside S, if w(u,v) < key[v], set key[v] = w(u,v) and remember parent v ← u.

Prim with a binary heap

import heapq

def prim(graph, root=0):
    n = len(graph)
    key = [float('inf')] * n
    parent = [-1] * n
    key[root] = 0
    heap = [(0, root)]
    in_mst = [False] * n
    total = 0
    while heap:
        k, u = heapq.heappop(heap)
        if in_mst[u]:
            continue
        in_mst[u] = True
        total += k
        for v, w in graph[u]:
            if not in_mst[v] and w < key[v]:
                key[v] = w
                parent[v] = u
                heapq.heappush(heap, (w, v))
    return parent, total

Prim from A on the Kruskal graph

Same undirected edges as Kruskal: MST by lightest edges (AB1, BC2, CD3, AC4, BD5). Grow Prim's tree from A.

  • S={A}; lightest out-edgeadd A–B weight 1
  • S={A,B}; cut edges AC4, BC2, BD5add B–C weight 2
  • S={A,B,C}; cut edges AC4, CD3, BD5add C–D weight 3
  • S completeMST edges AB,BC,CD; weight 6

Pro tip. Prim and Kruskal agree on the total weight (and can agree on the edge set). The difference is growth order: Prim expands one component; Kruskal merges a forest.

Prim's heap key for an outsider v stores
  1. The sum of all edge weights in the current tree S
  2. The weight of the lightest edge from S to v (or \infty if none)
  3. The DFS discovery time of v

Extract-min must pull the lightest cut edge into S. That is exactly the best edge from the tree to each outsider, maintained as key[v].

6Topological sort on a DAG

A topological order of a directed acyclic graph lists every vertex so that every edge u → v has u before v. Kahn's algorithm repeatedly peels vertices of in-degree 0; DFS records finish times and reverses them. Both run in O(V + E). Topological sort only exists for a DAG — if Kahn cannot place every vertex, a cycle remains.

Figure. Same DAG as the course-schedule ledger. Edges point from prerequisite to dependent course; one valid order is 0, 1, 2, 3.

Kahn's in-degree peel

  1. In-degreesCount incoming edges for every vertex; enqueue all with in-degree 0.
  2. PeelDequeue u, append u to the order, and decrement each neighbor's in-degree; enqueue any that hit 0.
  3. DetectIf the order length equals V, it is a valid topo order; leftovers with positive in-degree prove a cycle.

Kahn topological order

from collections import deque

def topo_kahn(n, edges):
    g = [[] for _ in range(n)]
    indeg = [0] * n
    for u, v in edges:
        g[u].append(v)
        indeg[v] += 1
    q = deque(i for i in range(n) if indeg[i] == 0)
    order = []
    while q:
        u = q.popleft()
        order.append(u)
        for v in g[u]:
            indeg[v] -= 1
            if indeg[v] == 0:
                q.append(v)
    return order if len(order) == n else None

Course Schedule (Topological Sort)

Four courses 0..3 with prerequisite edges 0→1, 0→2, 1→3, 2→3 (take the tail before the head). Find a valid order, or detect impossibility.

  • in-degrees0:0, 1:1, 2:1, 3:2
  • queue seed[0]
  • pop 0; unlock 1 and 2order [0]; queue [1,2]
  • pop 1 then 2; each decrements 3order [0,1,2]; 3 reaches in-degree 0
  • pop 3order [0,1,2,3]; all courses — valid DAG

Pro tip. Kahn's algorithm doubles as cycle detection: leftover nodes with nonzero in-degree mean the graph is not a DAG. DFS finish-time order (reversed) is the other standard construction.

Kahn's algorithm reports "impossible" when
  1. The graph has more edges than vertices
  2. The output order is shorter than V — some vertices never reached in-degree 0
  3. Two different valid orders exist

Vertices trapped on a cycle never hit in-degree 0, so they never enqueue. Multiple valid orders are normal on a DAG; that is not failure.

7Floyd-Warshall: all-pairs DP

Floyd-Warshall fills a V \times V distance matrix by trying every vertex k as an intermediate: dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]). After all k, every entry is a shortest path (or \infty if unreachable), assuming no negative cycle. Time O(V^3) and space O(V^2) — simpler than V Dijkstra runs on dense graphs, and it handles negatives the same way Bellman-Ford does (still no negative cycles).

Figure. Same three-vertex digraph as the Floyd ledger. Direct 1→3 costs 8; after k=2 the matrix holds 5 via vertex 2.

Triple loop over intermediates

  1. Seed matrixdist[i][i] = 0; dist[i][j] = w(i,j) for each edge; all other entries \infty.
  2. For each kAllow paths that may use vertex k (and previously considered intermediates).
  3. Relax i,jFor every pair (i,j), replace dist[i][j] if i → … → k → … → j is shorter.

Floyd-Warshall

def floyd_warshall(dist):
    n = len(dist)
    for k in range(n):
        for i in range(n):
            for j in range(n):
                through = dist[i][k] + dist[k][j]
                if through < dist[i][j]:
                    dist[i][j] = through
    return dist

All-pairs on three vertices

Vertices 1,2,3 with directed edges 1→2 weight 3, 1→3 weight 8, 2→3 weight 2, 3→1 weight 5. Run Floyd-Warshall (∞ = no direct edge).

  • initial dist rows [1],[2],[3][0,3,8], [∞,0,2], [5,∞,0]
  • after k=1 (through 1)[0,3,8], [∞,0,2], [5,8,0] — 3→2 via 1
  • after k=2 (through 2)[0,3,5], [∞,0,2], [5,8,0] — 1→3 via 2
  • after k=3 (through 3)[0,3,5], [7,0,2], [5,8,0] — 2→1 via 3

Pro tip. For dense all-pairs shortest paths, Floyd-Warshall's O(V^3) is simpler than running Dijkstra from every node. Check: 1→2→3 = 5 and 2→3→1 = 7 match the final matrix.

Floyd-Warshall's outermost loop index k means
  1. The source vertex of the query
  2. The highest-numbered vertex allowed as an intermediate on paths considered so far
  3. The hop limit — paths with more than k edges are ignored forever

At stage k the DP has considered intermediates from the first k vertices (in the vertex numbering used). It is not a hop cap and not a fixed source — every pair updates each round.

Notes

  • Dijkstra's Algorithm: Finds single-source shortest paths on graphs with non-negative edge weights using a min-priority queue; it fails with negative edges.
  • Bellman-Ford: Handles negative edge weights and detects negative cycles by relaxing all edges V-1 times.
  • Minimum Spanning Tree: Kruskal's adds edges in increasing weight using union-find to avoid cycles; Prim's grows the tree from a start node using a heap.
  • Topological Sort: Orders vertices of a DAG so every edge goes forward; computed via DFS finish times or Kahn's in-degree BFS.
  • Floyd-Warshall: Computes all-pairs shortest paths via dynamic programming over intermediate vertices.

Formulas

  • Dijkstra with a binary heap: O((V + E) \log V).
  • Bellman-Ford: O(VE) time; detects negative cycles.
  • Kruskal's MST: O(E \log E) (dominated by sorting edges); Prim's with a heap: O(E \log V).
  • Topological sort (Kahn's / DFS): O(V + E).
  • Floyd-Warshall all-pairs: O(V^3) time, O(V^2) space.

Exam traps & shortcuts

  • Non-negative weights => Dijkstra; negative weights or cycle detection needed => Bellman-Ford.
  • Topological sort only exists for a DAG; if Kahn's algorithm can't process all vertices, the graph has a cycle.
  • For dense all-pairs shortest paths, Floyd-Warshall's O(V^3) is simpler than running Dijkstra from every node.
  • Choose Kruskal for sparse graphs (edge-sorted with union-find); Prim is natural for dense graphs.

Reference tables

Heap logarithmic factors assume a binary heap. Fibonacci-heap Dijkstra improves the theoretical bound; interviews usually quote the binary-heap form.

Costs at a glance
AlgorithmTimeNotes
Dijkstra (binary heap)O((V+E)\log V)Non-negative weights only
Bellman-FordO(VE)Negatives OK; extra pass detects neg. cycles
Kruskal MSTO(E\log E)Sort + union-find; sparse-friendly
Prim MST (binary heap)O(E\log V)Grows one tree; dense-friendly
Topological sort (Kahn / DFS)O(V+E)DAG only; short order ⇒ cycle
Floyd-WarshallO(V^3) time, O(V^2) spaceAll-pairs; simple dense default

Unweighted hop distance stays with BFS in Trees and Graphs. Everything below assumes weighted edges unless noted.

Reach for which tool?
NeedReach forTrap
Single-source, weights ≥ 0DijkstraOne negative edge voids finalization
Single-source, negatives / cycle testBellman-FordDo not stop at V-1 if you must certify no cycle
All-pairs on dense graphFloyd-WarshallNegative cycle still corrupts the matrix
Cheapest connector, undirectedKruskal or PrimDirected "MST" is a different problem
Course order / build orderTopo (Kahn or DFS finish)Only DAGs; leftovers mean a cycle

Recap

Weights and query shape pick the shortest-path engine; MST and topo are different questions on different graph kinds.

Dijkstra
Non-negative only; extract-min finalizes; O((V+E)\log V) with a binary heap.
Bellman-Ford
Negatives OK; V-1 relax rounds; one more improving pass ⇒ negative cycle.
MST
Kruskal sorts edges + union-find; Prim grows one tree with a heap.
Topo
Kahn peels in-degree 0; order shorter than V means a cycle.
Floyd-Warshall
All-pairs by intermediate k; O(V^3) — dense and simple.
Picker
Non-neg ⇒ Dijkstra; neg/cycle ⇒ Bellman-Ford; dense all-pairs ⇒ Floyd.

Practise Advanced Graph Algorithms

Reading is free and needs no account. Practice, mocks and progress live in the app.

  • 5 exam-style questions on this topic, with explanations
  • A 6-question practice set that ends the chapter
  • Timed mocks scored with the real marking scheme
  • Readiness tracked per topic, kept on your device
Continue with Google — freeNo card, no trial. Works offline once installed.