Minimum Spanning Trees, Shortest Paths, A* & PageRank
Weighted graphs attach a numerical cost to each edge, enabling network-wide structural optimisation: spanning all vertices at minimum total connection cost (MST), finding minimum-weight paths (SSSP / APSP), guiding search with admissible heuristics (A*), and ranking network authority recursively (PageRank).
prim's & kruskal's mst algorithms.
Given a connected, undirected weighted graph , a Minimum Spanning Tree (MST) is a spanning tree with edges that minimises the total edge weight .
A cut partitions the vertices into two non-empty disjoint sets. The Cut Property guarantees that the minimum-weight edge with one endpoint in and the other in is always safe to include in an MST. Unlike Dijkstra's algorithm, both Prim's and Kruskal's algorithms work correctly on graphs with negative edge weights because comparing single cross-cut edges never relies on path prefix monotonicity.
Algorithm Prim_Array(G, v0):
For each vertex v in G do:
cost[v] <- infinity
prev[v] <- null
End do
cost[v0] <- 0
Q <- list of all vertices in G
While Q is not empty do:
u <- vertex in Q with minimum cost[u]
remove u from Q
For each neighbour v of u do:
If v in Q and weight(u, v) < cost[v] then:
cost[v] <- weight(u, v)
prev[v] <- u
End if
End do
End do
Return prev
End Algorithm Algorithm Prim_PQ(G, v0):
Q <- new Min-Priority Queue
For each vertex v in G do:
cost[v] <- infinity
prev[v] <- null
enqueue(Q, v, cost[v])
End do
// -infinity ensures v0 is pulled first even with negative edges!
cost[v0] <- -infinity
requeue(Q, v0, cost[v0])
While Q is not empty do:
u <- peek(Q); dequeue(Q)
For each (u, v) with weight w in Edges(G, u) do:
If v in Q and w < cost[v] then:
cost[v] <- w
prev[v] <- u
requeue(Q, v, w)
End if
End do
End do
Return prev
End Algorithm Algorithm Kruskal(G):
T <- Graph with all vertices V and no edges
L <- list of all edges in E, sorted by weight ascending
For each edge (u, v) in L do:
If adding (u, v) to T does not create a cycle then:
Add edge (u, v) to T
End If
If |E(T)| == |V| - 1 then:
Return T
End If
End For
Return T
End Algorithm | ALGORITHM & IMPLEMENTATION | TIME COMPLEXITY | OPTIMAL GRAPH REGIME |
|---|---|---|
| Prim's (Unsorted Array / Adjacency Matrix) | Best for dense graphs where (avoids heap log factor). | |
| Prim's (Binary Min-Heap Priority Queue) | Best for sparse graphs where . | |
| Kruskal's (Sorted Edges + Union-Find) | Dominated by sorting edges; ideal for sparse edge-list graphs. |
prim's manual table method & loop invariant proof.
HESS Manual Table Method for Hand-Tracing Prim's Algorithm
In the HESS study guide, Prim's algorithm is hand-traced using a (distance, parent) state table where each column represents a vertex and each row represents the state of the Priority Queue after adding a vertex to the MST:
- Create a table with columns for every vertex (in lexicographical/alphabetical order). Initialise all cells to
(∞, nil). - Row 1 (Initialisation): Set the starting (lexicographically first) vertex to
(0, nil)and all other vertices to(∞, nil). Circle(0, nil). - Update Neighbours: Add the circled vertex as the label of the next row. For every unvisited vertex , if the edge weight is smaller than 's current distance in the previous row, update cell to
(w(u, v), u); otherwise, copy its previous(distance, parent)entry down. - Select Minimum & Tie-Breaking: In the current row, circle the unvisited cell with the minimum distance, breaking any ties in lexicographical (alphabetical) order. Cross out / lock that column for subsequent rows.
- Repeat until all vertices have been circled; the circled
(distance, parent)pairs directly list the MST edges and their weights!
Formal Correctness Proof of Prim's Algorithm (Loop Invariant)
In the HESS notes, Prim's algorithm is proven correct by establishing the Loop Invariant: At the start of each iteration of the while loop, the subtree is a subgraph of some Minimum Spanning Tree of .
Initially, consists of a single starting vertex and zero edges. Since every spanning tree of contains all vertices in , is trivially a subgraph of every MST .
The loop terminates when contains all vertices and edges. Since is connected, acyclic, spans , and is a subgraph of an MST , is a Minimum Spanning Tree.
Suppose before an iteration, and Prim's chooses the minimum-weight edge crossing the cut between and . If , then holds immediately.
Otherwise, suppose . Adding to creates a unique cycle . Because and , the cycle must contain at least one other edge that also crosses the cut . Since Prim's selected the minimum-weight cross-cut edge, . Forming a new spanning tree gives total weight . Since is an MST, , so is also an MST and , preserving the loop invariant!
dijkstra's & bellman-ford algorithms.
The Single-Source Shortest Path problem asks for the minimum-weight path from a starting source vertex to every other vertex .
Algorithm Dijkstra(G, source):
let Q be a Min-Priority Queue
dist[source] <- 0
prev[source] <- undefined
For each vertex v in G do:
If v != source then:
dist[v] <- infinity
prev[v] <- undefined
End If
Q.insertWithPriority(v, dist[v])
End For
While Q is not empty do:
u <- Q.pullMin()
For each neighbour v of u do:
alt <- dist[u] + weight(u, v)
If alt < dist[v] then:
dist[v] <- alt
prev[v] <- u
Q.decreasePriority(v, alt)
End If
End For
End While
Return dist[], prev[]
End Algorithm Algorithm BellmanFord(G, source):
For each vertex v in V do:
dist[v] <- infinity
prev[v] <- null
End For
dist[source] <- 0
// Relax all edges |V| - 1 times
For i from 1 to |V| - 1 do:
For each edge (u, v) with weight w in E do:
If dist[u] + w < dist[v] then:
dist[v] <- dist[u] + w
prev[v] <- u
End If
End For
End For
// |V|-th pass: check for negative-weight cycles
For each edge (u, v) with weight w in E do:
If dist[u] + w < dist[v] then:
Error "Graph contains a negative-weight cycle"
End If
End For
Return dist[], prev[]
End Algorithm - Why Dijkstra Fails on Negative Edges: Dijkstra greedily locks in the vertex with the smallest tentative distance , assuming adding future edges can only increase path cost. A negative edge discovered later can reduce the cost to after has already been extracted from the Priority Queue.
- Why Bellman-Ford Relaxes Times: Any simple shortest path (without cycles) in a graph of vertices contains at most edges. Relaxing every edge in across passes ( time) guarantees optimal distances unless a negative-weight cycle is reachable, which is caught if any edge can still be relaxed on the -th pass.
warshall's & floyd-warshall's algorithms.
Both Warshall's Algorithm (transitive closure / reachability) and Floyd-Warshall's Algorithm (all-pairs shortest paths) use Dynamic Programming over an matrix (), incrementally allowing vertices as intermediate stepping stones. Both run in time and space.
In the HESS notes, Warshall's and Floyd-Warshall's qualify as Dynamic Programming for two foundational reasons: (1) they decompose paths into trivial subproblems (direct edges of length 1 when ) versus non-trivial subproblems (paths routed through intermediate vertices ), and (2) they exhibit overlapping subproblems (every path from to any via reuses the cached sub-path ). Furthermore, Floyd-Warshall is structurally identical to Warshall's algorithm under min-plus algebra (replacing Boolean with and Boolean with ).
Algorithm Warshall_Improved(A[1..n, 1..n]):
T <- A // k = 0 base case: Boolean adjacency matrix
For k from 1 to n do:
For i from 1 to n do:
// HESS Optimisation: only scan row j if i can reach k!
If T[i, k] == 1 then:
For j from 1 to n do:
T[i, j] <- T[i, j] OR T[k, j]
End For
End If
End For
End For
Return T
End Algorithm Algorithm FloydWarshall(G):
let dist be a |V| x |V| matrix initialised to infinity
For each vertex v in V do:
dist[v, v] <- 0
End For
For each edge (u, v) with weight w in E do:
dist[u, v] <- w
End For
For k from 1 to |V| do:
For i from 1 to |V| do:
For j from 1 to |V| do:
If dist[i, k] + dist[k, j] < dist[i, j] then:
dist[i, j] <- dist[i, k] + dist[k, j]
End If
End For
End For
End For
Return dist
End Algorithm Initially, the main diagonal entries are . If the graph contains a negative-weight cycle reachable from vertex , the path from around the cycle and back to has total cost , causing on the main diagonal!
a* search & admissible heuristics.
A* Search combines the exact cost-so-far of Dijkstra's algorithm with the goal-directed heuristic estimate of Best-First Search. Each node in the Min-Priority Queue is ordered by the evaluation function:
- : The exact known shortest path weight from the start node to .
- : The heuristic estimated cost from to the target goal node.
- Admissible Heuristic Requirement: A* is guaranteed to find the optimal (shortest) path if and only if is admissible—meaning for all , where is the true minimum cost to reach the goal. An admissible heuristic never overestimates the remaining distance (it is optimistic). Note that when everywhere, A* degenerates into Dijkstra's Algorithm.
Straight-line ("as the crow flies") distance between and :
Always admissible for physical road/spatial networks since no path can be shorter than a straight line.
Sum of horizontal and vertical grid displacements:
Admissible when movement is restricted to 4 cardinal grid directions (North, South, East, West) without diagonal shortcuts.
Algorithm A*(Graph, source, goal):
open <- Min-Priority Queue
For each vertex v in Graph do:
v.dist <- infinity
v.parent <- null
v.h <- h(v, goal)
End do
source.dist <- 0
source.value <- source.dist + source.h
enqueue(open, source, source.value)
While open is not empty do:
u <- peek(open); dequeue(open)
If u == goal then:
Return u.parent // Reconstruct optimal path
End if
For each neighbour v of u do:
If u.dist + weight(u, v) < v.dist then:
v.dist <- u.dist + weight(u, v)
v.value <- v.dist + v.h
v.parent <- u
If v in open then:
requeue(open, v, v.value)
Else:
enqueue(open, v, v.value)
End if
End if
End do
End do
Return failure
End Algorithm recursion & google pagerank algorithm.
PageRank models the web as a directed graph where vertices are webpages and directed edges are hyperlinks from page to page . A page's importance is defined recursively by the importance of the pages linking to it, divided by their total outbound links :
- : The PageRank of node at iteration (initialised at to for all nodes).
- : The damping factor (typically ), representing the probability that a random surfer clicks a link on the current page rather than teleporting to a random page ().
- : The set of pages that have an outbound link pointing into .
- : The out-degree (total outbound links from page ).
- Sink Nodes (Dead Ends,
outdegree == 0): If a page has no outbound links (), it is a sink node that would drain PageRank from the system. In the HESS specification, before running PageRank iterations, every sink node has virtual outbound edges added to every node in the graph, including itself (so ).
Worked Example from HESS Notes: 4-Node Web Graph (Nodes 1, 2, 3, 4 with Sink Node 2)
In the HESS study notes, consider the 4-node directed graph (, damping factor , teleportation term ) with directed edges:
- Node 1 links to Node 2 and Node 3 ().
- Node 2 has zero outbound links ()—it is a sink node! By the HESS sink-node rule, virtual outbound links are added from Node 2 to all 4 nodes (1, 2, 3, and 4, including Node 2 itself), giving .
- Node 3 links to Node 2 and Node 4 ().
- Node 4 links to Node 3 ().
| ITERATION | NODE 1 | NODE 2 (SINK) | NODE 3 | NODE 4 | SUM () |
|---|---|---|---|---|---|
| Iteration 0 () | 0.25 | 0.25 | 0.25 | 0.25 | 1.00 |
| Iteration 1 () | 0.09 | 0.30 | 0.41 | 0.20 | 1.00 |
| Iteration 2 () | 0.10 | 0.31 | 0.31 | 0.28 | 1.00 |
| Iteration 3 () | 0.10 | 0.28 | 0.38 | 0.23 | 1.00 |
| Iteration 4 () | 0.10 | 0.30 | 0.34 | 0.26 | 1.00 |
| Iteration 10 (Converged) | 0.10 (Rank 4) | 0.29 (Rank 2) | 0.36 (Rank 1) | 0.25 (Rank 3) | 1.00 () |
Algorithm PageRank(G, d, iterations):
N <- |V(G)|
// Step 1: Convert sink nodes (outdegree = 0) by linking to all nodes
For each node n in V(G) do:
If n.outdegree == 0 then:
For each node m in V(G) do:
add directed edge (n, m) to G
End do
n.outdegree <- N
End if
PR[n] <- 1 / N
End do
// Step 2: Iterative power-method update
For iter from 1 to iterations do:
For each node u in V(G) do:
sum <- 0
For each node v in Inbound(G, u) do:
sum <- sum + (PR[v] / v.outdegree)
End do
newPR[u] <- ((1 - d) / N) + d * sum
End do
PR <- newPR
End do
Return PR
End Algorithm Self-Check: Compare the capabilities of Dijkstra, Bellman-Ford, Prim, and Floyd-Warshall when negative edge weights are present.
- Prim's & Kruskal's (MST): Always work correctly with negative edge weights (even if negative cycles exist, since trees cannot contain cycles).
- Dijkstra's (SSSP): Fails on negative edge weights because once a vertex is popped from the Min-PQ, its distance is assumed final.
- Bellman-Ford (SSSP): Works with negative edge weights and detects reachable negative-weight cycles on the -th pass.
- Floyd-Warshall (APSP): Works with negative edge weights and detects negative-weight cycles whenever any diagonal entry becomes negative ().