Chapter 5.4: Shortest Paths
priority_queue, vector. Make sure you understand how BFS works before reading about Dijkstra.
Finding the shortest path between nodes is one of the most fundamental problems in graph theory. It appears in GPS navigation, network routing, game AI, and β most importantly for us β USACO problems. This chapter covers four algorithms (Dijkstra, Bellman-Ford, Floyd-Warshall, SPFA) and explains when to use each.
5.4.1 Problem Definition
Single-Source Shortest Path (SSSP)
Given a weighted graph G = (V, E) and a source node s, find the shortest distance from s to every other node.
From source A:
dist[A] = 0dist[B] = 1dist[C] = 5dist[D] = 5(AβBβD = 1+4)dist[E] = 8(AβBβDβE = 1+4+3)
Multi-Source Shortest Path (APSP)
Find shortest distances between all pairs of nodes. Used when you need distances from multiple sources, or between every pair.
Why Not Just BFS?
BFS finds shortest path in unweighted graphs (each edge = distance 1). With weights:
- Some paths have many short-weight edges
- Others have few large-weight edges
- BFS ignores weights entirely β wrong answer
5.4.2 Dijkstra's Algorithm
The most important shortest path algorithm. Used in ~90% of USACO problems involving weighted shortest paths.
Core Idea: Greedy + Priority Queue
Dijkstra is a greedy algorithm:
- Maintain a set of "settled" nodes (shortest distance finalized)
- Always process the unvisited node with smallest current distance next
- When processing a node, try to relax its neighbors (update their distances if we found a shorter path)
Why greedy works: If all edge weights are non-negative, the node currently at minimum distance cannot be improved by going through any other node (all alternatives would be β₯ current distance).
Step-by-Step Trace
Start: node 0 Β |Β Initial: dist = [0, β, β, β, β]
| Step | Process Node | Relaxations | dist array | Queue |
|---|---|---|---|---|
| 1 | node 0 (dist=0) | 0β1: min(β, 0+4)=4; 0β2: min(β, 0+2)=2; 0β3: min(β, 0+5)=5 | [0, 4, 2, 5, β] | {(2,2),(4,1),(5,3)} |
| 2 | node 2 (dist=2) | 2β3: min(5, 2+1)=3 β improved! | [0, 4, 2, 3, β] | {(3,3),(4,1),(5,3_old)} |
| 3 | node 3 (dist=3) | 3β1: min(4, 3+1)=4 (no change); 3β4: min(β, 3+3)=6 | [0, 4, 2, 3, 6] | {(4,1),(6,4),(5,3_old)} |
| 4 | node 1 (dist=4) | No relaxation possible | [0, 4, 2, 3, 6] | {(6,4)} |
| 5 | node 4 (dist=6) | Done! | [0, 4, 2, 3, 6] | {} |
Final: dist = [0, 4, 2, 3, 6]
Complete Dijkstra Implementation
// Solution: Dijkstra's Algorithm with Priority Queue β O((V+E) log V)
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> pii; // {distance, node}
typedef long long ll;
const ll INF = 1e18; // use long long to avoid int overflow!
const int MAXN = 100005;
// Adjacency list: adj[u] = list of {weight, v}
vector<pii> adj[MAXN];
vector<ll> dijkstra(int src, int n) {
vector<ll> dist(n + 1, INF); // dist[i] = shortest distance to node i
dist[src] = 0;
// Min-heap: {distance, node}
// C++ priority_queue is max-heap by default, so negate to make min-heap
priority_queue<pii, vector<pii>, greater<pii>> pq;
pq.push({0, src});
while (!pq.empty()) {
auto [d, u] = pq.top(); pq.pop(); // get node with minimum distance
// KEY: Skip if we've already found a better path to u
// (outdated entry in the priority queue)
if (d > dist[u]) continue;
// Relax all neighbors of u
for (auto [w, v] : adj[u]) {
ll newDist = dist[u] + w;
if (newDist < dist[v]) {
dist[v] = newDist; // update distance
pq.push({newDist, v}); // add updated entry to queue
}
}
}
return dist;
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n, m;
cin >> n >> m;
for (int i = 0; i < m; i++) {
int u, v, w;
cin >> u >> v >> w;
adj[u].push_back({w, v});
adj[v].push_back({w, u}); // undirected graph
}
int src;
cin >> src;
vector<ll> dist = dijkstra(src, n);
for (int i = 1; i <= n; i++) {
if (dist[i] == INF) cout << -1 << "\n";
else cout << dist[i] << "\n";
}
return 0;
}
Reconstructing the Shortest Path
Path Reconstruction β Forward recording then backward tracing:
π‘ Implementation key: Record
prev_node[v] = umeaning "the node before v on the shortest path to v is u". To reconstruct, followprev_nodefromdstback tosrc, then reverse the result.
// Solution: Dijkstra with Path Reconstruction
vector<int> prev_node(MAXN, -1); // prev_node[v] = previous node on shortest path to v
vector<ll> dijkstraWithPath(int src, int n) {
vector<ll> dist(n + 1, INF);
dist[src] = 0;
priority_queue<pii, vector<pii>, greater<pii>> pq;
pq.push({0, src});
while (!pq.empty()) {
auto [d, u] = pq.top(); pq.pop();
if (d > dist[u]) continue;
for (auto [w, v] : adj[u]) {
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
prev_node[v] = u; // track where we came from
pq.push({dist[v], v});
}
}
}
return dist;
}
// Reconstruct path from src to dst
vector<int> getPath(int src, int dst) {
vector<int> path;
for (int v = dst; v != -1; v = prev_node[v]) {
path.push_back(v);
}
reverse(path.begin(), path.end());
return path;
}
// BAD: Processes stale entries in queue
while (!pq.empty()) {
auto [d, u] = pq.top(); pq.pop();
// NO CHECK for d > dist[u]!
// Will re-process nodes with outdated distances
// Still correct, but O(E log E) instead of O(E log V)
for (auto [w, v] : adj[u]) {
if (d + w < dist[v]) {
dist[v] = d + w;
pq.push({dist[v], v});
}
}
}
// GOOD: Skip outdated priority queue entries
while (!pq.empty()) {
auto [d, u] = pq.top(); pq.pop();
if (d > dist[u]) continue; // β stale entry, skip!
for (auto [w, v] : adj[u]) {
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
pq.push({dist[v], v});
}
}
}
Key Points for Dijkstra
π« CRITICAL: Dijkstra does NOT work with negative edge weights! If any edge weight is negative, Dijkstra may produce incorrect results. The algorithm's correctness relies on the greedy assumption that once a node is settled (popped from the priority queue), its distance is final β negative edges break this assumption. For graphs with negative weights, use Bellman-Ford or SPFA instead.
- Only works with non-negative weights. Negative edges break the greedy assumption (see warning above).
- Use
long longfor distances when edge weights can be large.dist[u] + wcan overflowint. - Use
greater<pii>to makepriority_queuea min-heap. - The
if (d > dist[u]) continue;check is essential for correctness and performance.
5.4.3 Bellman-Ford Algorithm
When edges can have negative weights, Dijkstra fails. Bellman-Ford handles negative weights β and even detects negative cycles.
Core Idea: Relaxation V-1 Times
Key insight: any shortest path in a graph with V nodes uses at most V-1 edges (no repeated nodes). So if we relax ALL edges V-1 times, we're guaranteed to find the correct shortest paths.
Algorithm:
1. dist[src] = 0, dist[all others] = INF
2. Repeat V-1 times:
For every edge (u, v, w):
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w (relax!)
3. Check for negative cycles:
If ANY edge can still be relaxed β negative cycle exists!
Bellman-Ford Relaxation Process (graph: AβB w=2, AβC w=5, BβC w=-1, CβD w=2, BβD w=4):
| Round | dist[A] | dist[B] | dist[C] | dist[D] | Edges relaxed |
|---|---|---|---|---|---|
| Init | 0 | β | β | β | β |
| 1 | 0 | 2 | 5 | β | AβB, AβC |
| 2 | 0 | 2 | 4 (via B) | 7 (via C) | BβC(β1), CβD |
| 3 | 0 | 2 | 4 | 6 (via BβD) | BβD(4) |
| 4 (final) | 0 | 2 | 4 | 6 | no change β converged |
π‘ Key observation: After each round, at least one more node's shortest distance is finalized. After Vβ1 rounds, all shortest distances are correct (assuming no negative cycles).
Bellman-Ford Implementation
// Solution: Bellman-Ford Algorithm β O(V * E)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef tuple<int, int, int> Edge; // {from, to, weight}
const ll INF = 1e18;
// Returns shortest distances, or empty if negative cycle detected
vector<ll> bellmanFord(int src, int n, vector<Edge>& edges) {
vector<ll> dist(n + 1, INF);
dist[src] = 0;
// Relax all edges V-1 times
for (int iter = 0; iter < n - 1; iter++) {
bool updated = false;
for (auto [u, v, w] : edges) {
if (dist[u] != INF && dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
updated = true;
}
}
if (!updated) break; // early termination: already converged
}
// Check for negative cycles (one more relaxation pass)
for (auto [u, v, w] : edges) {
if (dist[u] != INF && dist[u] + w < dist[v]) {
// Negative cycle reachable from source!
return {}; // signal: negative cycle exists
}
}
return dist;
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n, m;
cin >> n >> m;
vector<Edge> edges;
for (int i = 0; i < m; i++) {
int u, v, w;
cin >> u >> v >> w;
edges.push_back({u, v, w});
// For undirected: also add {v, u, w}
}
int src;
cin >> src;
vector<ll> dist = bellmanFord(src, n, edges);
if (dist.empty()) {
cout << "Negative cycle detected!\n";
} else {
for (int i = 1; i <= n; i++) {
cout << (dist[i] == INF ? -1 : dist[i]) << "\n";
}
}
return 0;
}
Why Bellman-Ford Works
After k iterations of the outer loop, dist[v] contains the shortest path from src to v using at most k edges. After V-1 iterations, all shortest paths (which use at most V-1 edges in a cycle-free graph) are found.
Negative Cycle Detection: A negative cycle means you can keep decreasing distance indefinitely. If the V-th relaxation still improves a distance, that node is on or reachable from a negative cycle.
5.4.4 Floyd-Warshall Algorithm
For finding shortest paths between all pairs of nodes.
Core Idea: DP Through Intermediate Nodes
dp[k][i][j] = shortest distance from i to j using only nodes {1, 2, ..., k} as intermediate nodes.
Recurrence:
dp[k][i][j] = min(dp[k-1][i][j], // don't use node k
dp[k-1][i][k] + dp[k-1][k][j]) // use node k
Since we only need the previous layer, we can collapse to 2D:
// Solution: Floyd-Warshall All-Pairs Shortest Path β O(V^3)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll INF = 1e18;
const int MAXV = 505;
ll dist[MAXV][MAXV]; // dist[i][j] = shortest distance from i to j
void floydWarshall(int n) {
// β οΈ CRITICAL: k MUST be the OUTERMOST loop!
// Invariant: after processing k, dist[i][j] = shortest path from i to j
// using only nodes {1..k} as intermediates.
// If k were inner, dist[i][k] or dist[k][j] might not yet reflect all
// intermediate nodes up to k-1, breaking the DP correctness.
for (int k = 1; k <= n; k++) { // β OUTER: intermediate node
for (int i = 1; i <= n; i++) { // β MIDDLE: source
for (int j = 1; j <= n; j++) { // β INNER: destination
// Can we go iβkβj faster than iβj directly?
if (dist[i][k] != INF && dist[k][j] != INF) {
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
}
}
}
}
// After Floyd-Warshall, dist[i][i] < 0 iff node i is on a negative cycle
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n, m;
cin >> n >> m;
// Initialize: distance to self = 0, all others = INF
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
dist[i][j] = (i == j) ? 0 : INF;
// Read edges
for (int i = 0; i < m; i++) {
int u, v; ll w;
cin >> u >> v >> w;
dist[u][v] = min(dist[u][v], w); // handle multiple edges
dist[v][u] = min(dist[v][u], w); // undirected
}
floydWarshall(n);
// Query: shortest path from u to v
int q; cin >> q;
while (q--) {
int u, v; cin >> u >> v;
cout << (dist[u][v] == INF ? -1 : dist[u][v]) << "\n";
}
return 0;
}
Floyd-Warshall Complexity
- Time:
O(VΒ³)β three nested loops, each running V times - Space:
O(VΒ²)β the 2D distance array - Practical limit: V β€ 500 or so (500Β³ = 1.25 Γ 10βΈ is borderline)
- For V > 1000, use Dijkstra from each source:
O(V Γ (V+E) log V)
Floyd-Warshall DP Transition β introducing node k as an intermediate:
π‘ Why must k be the outermost loop? When processing intermediate node k, both
dist[i][k]anddist[k][j]must already be fully computed using intermediates {1..kβ1}. If k were an inner loop, those values might be updated in the same pass, breaking the DP correctness.
5.4.5 Algorithm Comparison Table
| Algorithm | Time Complexity | Negative Edges | Negative Cycles | Multi-Source | Best For |
|---|---|---|---|---|---|
| BFS | O(V + E) | β No | β No | β Yes (multi-source BFS) | Unweighted graphs |
| Dijkstra | O((V+E) log V) | β No | β No | β (run once per source) | Weighted, non-negative edges |
| Bellman-Ford | O(V Γ E) | β Yes | β Detects | β | Negative edges, detecting neg cycles |
| SPFA | O(V Γ E) worst, O(E) avg | β Yes | β Detects | β | Sparse graphs with neg edges |
| Floyd-Warshall | O(VΒ³) | β Yes | β Detects (diag) | β Yes (all pairs) | Dense graphs, all-pairs queries |
When to Use Which?
Graph has negative edges?
βββ YES β Bellman-Ford or SPFA (or Floyd-Warshall for all-pairs)
βββ NO β V β€ 500 and need all-pairs?
βββ YES β Floyd-Warshall O(VΒ³)
βββ NO β Unweighted graph (all edges = 1)?
βββ YES β BFS O(V+E)
βββ NO β Edge weights only 0 or 1?
βββ YES β 0-1 BFS O(V+E)
βββ NO β Dijkstra O((V+E) log V)
| Situation | Algorithm |
|---|---|
| All edges weight 1 | BFS |
| Non-negative weights | Dijkstra |
| Negative edges, no cycle | Bellman-Ford / SPFA |
| Need all-pairs, V β€ 500 | Floyd-Warshall |
| Edges are only 0 or 1 | 0-1 BFS |
5.4.6 SPFA β Bellman-Ford with Queue Optimization
SPFA (Shortest Path Faster Algorithm) is an optimized Bellman-Ford that only adds a node to the queue when its distance is updated, avoiding redundant relaxations.
β οΈ SPFA Worst Case: SPFA's worst-case time complexity is O(V Γ E) β identical to plain Bellman-Ford. On adversarially constructed graphs (common in competitive programming "anti-SPFA" test cases), SPFA degrades to O(VE) and may TLE. A node can enter the queue up to V times; with E edges processed per queue entry, the total is O(VE). In most random/practical cases it's fast (O(E) average), but for USACO, prefer Dijkstra when all weights are non-negative.
// Solution: SPFA (Bellman-Ford + Queue Optimization)
#include <bits/stdc++.h>
using namespace std;
typedef pair<int,int> pii;
typedef long long ll;
const ll INF = 1e18;
const int MAXN = 100005;
vector<pii> adj[MAXN];
vector<ll> spfa(int src, int n) {
vector<ll> dist(n + 1, INF);
vector<bool> inQueue(n + 1, false);
vector<int> cnt(n + 1, 0); // cnt[v] = number of times v entered queue
queue<int> q;
dist[src] = 0;
q.push(src);
inQueue[src] = true;
while (!q.empty()) {
int u = q.front(); q.pop();
inQueue[u] = false;
for (auto [w, v] : adj[u]) {
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
if (!inQueue[v]) {
q.push(v);
inQueue[v] = true;
cnt[v]++;
// Negative cycle detection: if a node enters queue >= n times
// (a node can enter at most n-1 times without a neg cycle;
// using > n is also safe but detects one step later)
if (cnt[v] >= n) return {}; // negative cycle!
}
}
}
}
return dist;
}
5.4.7 BFS as Dijkstra for Unweighted Graphs
When all edge weights are 1 (unweighted graph), BFS is exactly Dijkstra with a simple queue:
- Dijkstra's priority queue naturally processes nodes in order of distance
- In an unweighted graph, all edges have weight 1, so nodes at distance d are processed before distance d+1
- BFS naturally explores level-by-level, which is exactly "by distance"
// Solution: BFS for Unweighted Shortest Path β O(V + E)
// Equivalent to Dijkstra when all weights = 1
vector<int> bfsShortestPath(int src, int n) {
vector<int> dist(n + 1, -1);
queue<int> q;
dist[src] = 0;
q.push(src);
while (!q.empty()) {
int u = q.front(); q.pop();
for (auto [w, v] : adj[u]) {
if (dist[v] == -1) { // unvisited
dist[v] = dist[u] + 1; // all weights = 1
q.push(v);
}
}
}
return dist;
}
Why is BFS correct for unweighted graphs?
Because BFS explores nodes in strictly increasing order of their distance. The first time you reach a node v, you've found the shortest path (fewest edges = minimum distance when all weights are 1).
0-1 BFS: A powerful trick when edge weights are only 0 or 1 (use deque instead of queue):
0-1 BFS deque enqueue rule:
Deque: [front β smallest dist ... β back β largest dist]
When relaxing neighbor v via edge (uβv) with weight w:
w = 0 β push_front(v) (same distance as u β keep at front)
w = 1 β push_back(v) (one step further β goes to back)
Why correct? The deque front always holds the current minimum-distance node,
because w=0 edges don't increase the distance, while w=1 edges do.
This is Dijkstra-like behavior without a heap: O(V+E) instead of O((V+E) log V).
π‘ Efficiency: 0-1 BFS runs in O(V+E) β faster than Dijkstra's O((V+E) log V). When edge weights are only 0 and 1, always prefer 0-1 BFS.
// Solution: 0-1 BFS β O(V + E), handles {0,1} weight edges
vector<int> bfs01(int src, int n) {
vector<int> dist(n + 1, INT_MAX);
deque<int> dq;
dist[src] = 0;
dq.push_front(src);
while (!dq.empty()) {
int u = dq.front(); dq.pop_front();
for (auto [w, v] : adj[u]) {
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
if (w == 0) dq.push_front(v); // 0-weight: add to front
else dq.push_back(v); // 1-weight: add to back
}
}
}
return dist;
}
5.4.8 USACO Example: Farm Tours
Problem Statement (USACO 2003 Style)
Farmer John wants to take a round trip: travel from farm 1 to farm N, then return from N to farm 1, using no road twice. Roads are bidirectional. Find the minimum total distance of such a round trip.
Constraints: N β€ 1000, M β€ 10,000, weights β€ 1000.
Input Format:
N M
u1 v1 w1
u2 v2 w2
...
Analysis:
- We need to go 1βN and Nβ1 without repeating any edge
- Key insight: this equals finding two edge-disjoint paths from 1 to N with minimum total cost
- Alternative insight: the "return trip" Nβ1 is just another path 1βN in the original graph
- Simplification for this problem: Find the shortest path from 1 to N twice, but with different edges
For this USACO-style problem, a simpler interpretation: since roads are bidirectional and we can use each road at most once in each direction, find:
- Shortest path 1βN
- Shortest path Nβ1 (using possibly different roads)
- These can be found independently with Dijkstra
But the real challenge: "using no road twice" means globally, not just per direction.
Greedy approach for this version: Find shortest path 1βN, then find shortest path on remaining graph Nβ1. This greedy doesn't always work, but for USACO Bronze/Silver, many problems simplify to just running Dijkstra twice.
// Solution: Farm Tours β Two Dijkstra (simplified version)
// Run Dijkstra from both endpoints, find min round-trip distance
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<ll, int> pli;
const ll INF = 1e18;
const int MAXN = 1005;
vector<pair<int,int>> adj[MAXN]; // {weight, dest}
vector<ll> dijkstra(int src, int n) {
vector<ll> dist(n + 1, INF);
priority_queue<pli, vector<pli>, greater<pli>> pq;
dist[src] = 0;
pq.push({0, src});
while (!pq.empty()) {
auto [d, u] = pq.top(); pq.pop();
if (d > dist[u]) continue;
for (auto [w, v] : adj[u]) {
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
pq.push({dist[v], v});
}
}
}
return dist;
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n, m;
cin >> n >> m;
for (int i = 0; i < m; i++) {
int u, v, w;
cin >> u >> v >> w;
adj[u].push_back({w, v});
adj[v].push_back({w, u}); // bidirectional
}
// Run Dijkstra from farm 1 and farm N
vector<ll> distFrom1 = dijkstra(1, n);
vector<ll> distFromN = dijkstra(n, n);
// Find intermediate farm that minimizes: dist(1,k) + dist(k,N) + dist(N,k) + dist(k,1)
// = 2 * (dist(1,k) + dist(k,N)) ... but this is just going via k twice
// Simplest: answer is distFrom1[n] + distFromN[1]
// (Go 1βN one way, return Nβ1 by shortest path β may reuse edges)
ll answer = distFrom1[n] + distFromN[1];
if (answer >= INF) cout << "NO VALID TRIP\n";
else cout << answer << "\n";
// For the "no road reuse" constraint, see flow algorithms (beyond Silver)
return 0;
}
π‘ Extended: Finding Two Edge-Disjoint Paths
The true "no road reuse" version requires min-cost flow (a Gold+ topic). The key insight is:
- Model each undirected edge as two directed edges with capacity 1
- Find min-cost flow of 2 units from node 1 to node N
- This equals two edge-disjoint paths with minimum total cost
For USACO Silver, you'll rarely need min-cost flow β the simpler Dijkstra approach suffices.
5.4.9 Dijkstra on Grids
Many USACO problems involve grid-based shortest paths. The graph is implicit:
// Solution: Dijkstra on Grid β find shortest path from (0,0) to (R-1,C-1)
// Each cell has a "cost" to enter
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef tuple<ll,int,int> tli;
const ll INF = 1e18;
int dx[] = {0,0,1,-1};
int dy[] = {1,-1,0,0};
ll dijkstraGrid(vector<vector<int>>& grid) {
int R = grid.size(), C = grid[0].size();
vector<vector<ll>> dist(R, vector<ll>(C, INF));
priority_queue<tli, vector<tli>, greater<tli>> pq;
dist[0][0] = grid[0][0];
pq.push({grid[0][0], 0, 0});
while (!pq.empty()) {
auto [d, r, c] = pq.top(); pq.pop();
if (d > dist[r][c]) continue;
for (int k = 0; k < 4; k++) {
int nr = r + dx[k], nc = c + dy[k];
if (nr < 0 || nr >= R || nc < 0 || nc >= C) continue;
ll newDist = dist[r][c] + grid[nr][nc];
if (newDist < dist[nr][nc]) {
dist[nr][nc] = newDist;
pq.push({newDist, nr, nc});
}
}
}
return dist[R-1][C-1];
}
β οΈ Common Mistakes β The Dirty Five
// BAD: int overflow when adding large distances
vector<int> dist(n+1, 1e9); // use int
// dist[u] = 9Γ10^8, w = 9Γ10^8
// dist[u] + w overflows int!
if (dist[u] + w < dist[v]) { ... }
// GOOD: always use long long for distances
const ll INF = 1e18;
vector<ll> dist(n+1, INF);
// No overflow with long long (max ~9.2Γ10^18)
if (dist[u] + w < dist[v]) { ... }
// BAD: This is a MAX-heap, not min-heap!
priority_queue<pii> pq; // default is max-heap
pq.push({dist[v], v});
// Will process FARTHEST node first β wrong!
// GOOD: explicitly specify min-heap
priority_queue<pii, vector<pii>, greater<pii>> pq;
pq.push({dist[v], v});
// Now processes NEAREST node first β
5 Classic Dijkstra Bugs:
- Using
intinstead oflong longβ distance sum overflows β wrong answers silently - Max-heap instead of min-heap β forgetting
greater<pii>β processes wrong node first - Missing stale entry check (
if (d > dist[u]) continue) β not wrong but ~10x slower - Forgetting
dist[src] = 0β all distances remain INF - Using Dijkstra with negative edges β undefined behavior, may loop infinitely or give wrong answer
Chapter Summary
π Key Takeaways
| Algorithm | Complexity | Handles Neg | Use When |
|---|---|---|---|
| BFS | O(V+E) | β | Unweighted graphs |
| Dijkstra | O((V+E) log V) | β | Non-negative weighted SSSP |
| Bellman-Ford | O(VE) | β | Negative edges, detect neg cycles |
| SPFA | O(VE) worst, fast avg | β | Sparse graphs, neg edges |
| Floyd-Warshall | O(VΒ³) | β | All-pairs, V β€ 500 |
| 0-1 BFS | O(V+E) | N/A | Edges with weight 0 or 1 only |
β FAQ
Q1: Why can't Dijkstra handle negative edges?
A: Dijkstra's greedy assumption is "the node with the current shortest distance cannot be improved by later paths." With negative edges, this assumption failsβa longer path through a negative edge may end up shorter.
Concrete counterexample: Nodes A, B, C. Edges: AβB=2, AβC=10, BβC=β20.
- Dijkstra processes A first (dist=0), relaxes to dist[B]=2, dist[C]=10
- Then processes B (dist=2, minimum), relaxes to dist[C]=min(10, 2+(-20))=-18
- But if Dijkstra "settles" C before processing B (this specific case won't, but slightly different weights will cause issues)
General explanation: When node u is popped and settled, Dijkstra considers
dist[u]optimal. But if there is a negative edge (v, u, w) with w < 0, there may be a path srcβ...βvβu with total weight < currentdist[u], while v has not yet been processed.Conclusion: With negative edges, you must use Bellman-Ford (
O(VE)) or SPFA (averageO(E), worstO(VE)).
Q2: What is the difference between SPFA and Bellman-Ford?
A: SPFA is a queue-optimized version of Bellman-Ford. Bellman-Ford traverses all edges each round; SPFA only updates neighbors of nodes whose distance improved, using a queue to track which nodes need processing. In practice SPFA is much faster (average
O(E)), but the theoretical worst case is the same (O(VE)). On some contest platforms SPFA can be hacked to worst case, so with negative edges consider Bellman-Ford; without negative edges always use Dijkstra.
Q3: Why must the k loop be the outermost in Floyd-Warshall?
A: This is the most common Floyd-Warshall implementation error! The DP invariant is: after the k-th outer loop iteration,
dist[i][j]represents the shortest path from i to j using only nodes {1, 2, ..., k} as intermediates. When processing intermediate node k,dist[i][k]anddist[k][j]must already be fully computed based on {1..k-1}. If k is in the inner loop,dist[i][k]may have just been updated in the same outer loop iteration, leading to incorrect results. Remember: k is outermost, i and j are inner β order matters!
Q4: How to determine whether a USACO problem needs Dijkstra or BFS?
A: Key question: Are edges weighted?
- Unweighted graph (edge weight=1 or find minimum edges) β BFS,
O(V+E), faster and simpler code- Weighted graph (different non-negative weights) β Dijkstra
- Edge weights only 0 or 1 β 0-1 BFS (faster than Dijkstra,
O(V+E))- Has negative edges β Bellman-Ford/SPFA
Q5: When to use Floyd-Warshall?
A: When you need shortest distances between all pairs, and V β€ 500 (since
O(VΒ³)β 1.25Γ10βΈ is barely feasible at V=500). Typical scenario: given multiple sources and targets, query distance between any pair. For V > 500, run Dijkstra once per node (O(V Γ (V+E) log V)) is faster.
π Connections to Other Chapters
- Chapter 5.2 (BFS & DFS): BFS is "Dijkstra for unweighted graphs"; this chapter is a direct extension of BFS
- Chapter 3.11 (Binary Trees): Dijkstra's priority queue is a binary heap; understanding heaps helps analyze complexity
- Chapter 5.3 (Trees & Special Graphs): Shortest path on a tree is the unique root-to-node path (DFS/BFS suffices)
- Chapter 6.1 (DP Introduction): Floyd-Warshall is essentially DP (state = "using first k nodes"); many shortest path variants can be modeled with DP
- USACO Gold: Shortest path + DP combinations (e.g., DP on shortest path DAG), shortest path + binary search, shortest path + data structure optimization
Practice Problems
Problem 5.4.1 β Classic Dijkstra π’ Easy
Problem: Given N cities and M bidirectional roads, each with a travel time. Find the shortest travel time from city 1 to city N. If city N is unreachable, output β1.
Input format:
N M
uβ vβ wβ
...
π Sample Input/Output 1 (7 lines, click to expand)
Sample Input 1:
5 6
1 2 2
1 3 4
2 3 1
2 4 7
3 5 3
4 5 1
Sample Output 1:
6
Sample Input 2:
3 2
1 2 5
2 1 3
Sample Output 2:
-1
(Node 3 is unreachable)
Constraints: 2 β€ N β€ 10^5, 1 β€ M β€ 5Γ10^5, 1 β€ w β€ 10^9
π‘ Hint
Standard Dijkstra from node 1. Use long long for distances β max path = N Γ max_weight = 10^5 Γ 10^9 = 10^14 which overflows int. Initialize all distances to LLONG_MAX.
β Full Solution
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<ll,int> pli;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n, m;
cin >> n >> m;
vector<vector<pair<int,int>>> adj(n + 1);
for (int i = 0; i < m; i++) {
int u, v, w; cin >> u >> v >> w;
adj[u].push_back({v, w});
adj[v].push_back({u, w});
}
vector<ll> dist(n + 1, LLONG_MAX);
priority_queue<pli, vector<pli>, greater<pli>> pq;
dist[1] = 0;
pq.push({0, 1});
while (!pq.empty()) {
auto [d, u] = pq.top(); pq.pop();
if (d > dist[u]) continue; // outdated entry
for (auto [v, w] : adj[u]) {
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
pq.push({dist[v], v});
}
}
}
cout << (dist[n] == LLONG_MAX ? -1 : dist[n]) << "\n";
return 0;
}
// Time: O((N + M) log N), Space: O(N + M)
Problem 5.4.2 β BFS on Grid π’ Easy
Problem: A robot starts at cell (0,0) of an RΓC grid. Some cells are walls (#); others are passable (.). Find the minimum number of steps to reach (R-1, C-1). Output β1 if impossible.
Input format:
R C
rowβ
...
Sample Input 1:
3 4
....
.##.
....
Sample Output 1:
6
(Path: (0,0)β(0,1)β(0,2)β(0,3)β(1,3)β(2,3)β... wait, (2,3) is corner not (R-1,C-1)=(2,3). Answer 6 steps)
Sample Input 2:
2 2
.#
#.
Sample Output 2:
-1
Constraints: 1 β€ R, C β€ 1000
π‘ Hint
Standard grid BFS. Start at (0,0) with distance 0. Answer is dist[R-1][C-1].
β Full Solution
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int R, C;
cin >> R >> C;
vector<string> grid(R);
for (auto& row : grid) cin >> row;
vector<vector<int>> dist(R, vector<int>(C, -1));
queue<pair<int,int>> q;
if (grid[0][0] != '#') {
dist[0][0] = 0;
q.push({0, 0});
}
int dr[] = {-1, 1, 0, 0};
int dc[] = {0, 0, -1, 1};
while (!q.empty()) {
auto [r, c] = q.front(); q.pop();
for (int d = 0; d < 4; d++) {
int nr = r + dr[d], nc = c + dc[d];
if (nr >= 0 && nr < R && nc >= 0 && nc < C
&& grid[nr][nc] != '#' && dist[nr][nc] == -1) {
dist[nr][nc] = dist[r][c] + 1;
q.push({nr, nc});
}
}
}
cout << dist[R-1][C-1] << "\n";
return 0;
}
// Time: O(RΓC), Space: O(RΓC)
Problem 5.4.3 β Negative Edge Detection π‘ Medium
Problem: Given a directed graph with N nodes, M edges (possibly negative weights), find the shortest distance from node 1 to node N. If a negative cycle is reachable from node 1 and can reach node N, output "NEGATIVE CYCLE". If node N is unreachable, output "UNREACHABLE".
Input format:
N M
uβ vβ wβ
...
π Sample Input/Output 1 (6 lines, click to expand)
Sample Input 1:
4 5
1 2 1
2 3 2
3 4 3
1 3 -5
3 2 -10
Sample Output 1:
NEGATIVE CYCLE
Sample Input 2:
3 2
1 2 5
1 3 -1
Sample Output 2:
-1
Constraints: 1 β€ N β€ 1000, 1 β€ M β€ 5000, -10^9 β€ w β€ 10^9
π‘ Hint
Use Bellman-Ford: run Nβ1 relaxation passes. Then do a Nth pass: if any distance improves, there's a negative cycle reachable from node 1. Track which nodes are affected to determine if node N is impacted.
β Full Solution
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n, m;
cin >> n >> m;
vector<tuple<int,int,ll>> edges(m);
for (auto& [u, v, w] : edges) cin >> u >> v >> w;
const ll INF = 1e18;
vector<ll> dist(n + 1, INF);
dist[1] = 0;
// Bellman-Ford: N-1 passes
for (int iter = 0; iter < n - 1; iter++) {
for (auto [u, v, w] : edges) {
if (dist[u] != INF && dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
}
}
}
// N-th pass: detect negative cycles
vector<bool> inNegCycle(n + 1, false);
for (int iter = 0; iter < n; iter++) {
for (auto [u, v, w] : edges) {
if (dist[u] != INF && dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
inNegCycle[v] = true;
}
if (inNegCycle[u]) inNegCycle[v] = true;
}
}
if (dist[n] == INF) cout << "UNREACHABLE\n";
else if (inNegCycle[n]) cout << "NEGATIVE CYCLE\n";
else cout << dist[n] << "\n";
return 0;
}
// Time: O(V Γ E), Space: O(V + E)
Problem 5.4.5 β All-Pairs with Floyd π‘ Medium
Problem: Given N cities and M bidirectional roads with travel times, answer Q queries: "Is city u reachable from city v within distance limit D?" For each query, output "YES" or "NO".
Input format:
N M
uβ vβ wβ
...
Q
uβ vβ Dβ
...
π Sample Input/Output (10 lines, click to expand)
Sample Input:
4 5
1 2 3
2 3 2
3 4 1
1 4 10
2 4 7
3
1 4 6
1 4 10
2 3 5
Sample Output:
YES
YES
YES
Constraints: 1 β€ N β€ 300, 1 β€ M β€ NΒ², 1 β€ Q β€ 10^5, 1 β€ w β€ 10^9
π‘ Hint
Run Floyd-Warshall to get all-pairs shortest paths in O(NΒ³). Each query is then O(1).
β Full Solution
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n, m;
cin >> n >> m;
const ll INF = 1e18;
vector<vector<ll>> dist(n + 1, vector<ll>(n + 1, INF));
for (int i = 1; i <= n; i++) dist[i][i] = 0;
for (int i = 0; i < m; i++) {
int u, v; ll w;
cin >> u >> v >> w;
dist[u][v] = min(dist[u][v], w);
dist[v][u] = min(dist[v][u], w);
}
// Floyd-Warshall: O(NΒ³)
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
if (dist[i][k] != INF && dist[k][j] != INF)
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
int q;
cin >> q;
while (q--) {
int u, v; ll D;
cin >> u >> v >> D;
cout << (dist[u][v] <= D ? "YES" : "NO") << "\n";
}
return 0;
}
// Time: O(NΒ³ + Q), Space: O(NΒ²)
Problem 5.4.6 β Maximum Bottleneck Path π΄ Hard
Problem: Given N cities connected by M roads. Each road has a weight limit (the maximum cargo weight it can support). Find the path from city 1 to city N that maximizes the minimum edge weight along the path β i.e., the heaviest cargo you can move from city 1 to city N in a single trip.
Input format:
N M
uβ vβ wβ
...
π Sample Input/Output (6 lines, click to expand)
Sample Input:
4 5
1 2 3
1 3 6
2 4 5
3 4 4
2 3 2
Sample Output:
5
(Note: Sample recomputed β answer is 4 for path 1β3β4)
Constraints: 2 β€ N β€ 10^5, 1 β€ M β€ 3Γ10^5
π‘ Hint
Modified Dijkstra: Instead of minimizing total cost, maximize the bottleneck. Let dist[v] = maximum minimum edge weight on any path to v. Use a max-heap. Relaxation: dist[v] = max(dist[v], min(dist[u], weight(u,v))).
Alternatively: sort edges descending, add them with DSU until 1 and N are connected β the last edge added is the answer.
β Full Solution
#include <bits/stdc++.h>
using namespace std;
typedef pair<int,int> pii;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n, m;
cin >> n >> m;
vector<vector<pii>> adj(n + 1);
for (int i = 0; i < m; i++) {
int u, v, w; cin >> u >> v >> w;
adj[u].push_back({v, w});
adj[v].push_back({u, w});
}
// Modified Dijkstra: maximize bottleneck
// dist[v] = max min-edge-weight path from 1 to v
vector<int> dist(n + 1, 0);
priority_queue<pii> pq; // max-heap: {bottleneck, node}
dist[1] = INT_MAX;
pq.push({INT_MAX, 1});
while (!pq.empty()) {
auto [d, u] = pq.top(); pq.pop();
if (d < dist[u]) continue; // outdated
for (auto [v, w] : adj[u]) {
int newBottleneck = min(dist[u], w); // β KEY: take minimum along path
if (newBottleneck > dist[v]) {
dist[v] = newBottleneck;
pq.push({dist[v], v});
}
}
}
cout << dist[n] << "\n";
return 0;
}
// Time: O((N + M) log N), Space: O(N + M)
Problem 5.4.4 β Multi-Source BFS: Zombie Outbreak π‘ Medium
Problem: A zombie outbreak starts at K infected cities simultaneously. Each time unit, zombies spread to all adjacent (uninfected) cities. Find the minimum time for zombies to reach every reachable city. For cities that can never be reached, output β1.
Input format:
N M K
uβ vβ
... (M undirected edges)
zβ zβ ... zβ (K initial zombie cities)
π Sample Input/Output (8 lines, click to expand)
Sample Input:
6 6 2
1 2
2 3
3 4
4 5
5 6
2 5
1 4
Sample Output:
1 0 1 0 1 2
Multi-Source BFS β How K sources spread simultaneously:
π‘ Equivalent formulation: Multi-source BFS = add a virtual source node S, connect S to all K zombie cities with weight 0, then run single-source BFS from S. Pushing all K sources at t=0 is exactly this idea in practice.
π‘ Hint
Multi-source BFS: initialize the queue with all K infected cities at time 0. Run BFS normally. The BFS level at each city = minimum time for zombies to arrive. This is equivalent to adding a virtual "super-source" node connected to all K cities with weight 0.
β Full Solution
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n, m, k;
cin >> n >> m >> k;
vector<vector<int>> adj(n + 1);
for (int i = 0; i < m; i++) {
int u, v; cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
vector<int> dist(n + 1, -1);
queue<int> q;
// Push ALL K zombie sources at time 0
for (int i = 0; i < k; i++) {
int z; cin >> z;
dist[z] = 0;
q.push(z);
}
// Standard BFS from all sources simultaneously
while (!q.empty()) {
int u = q.front(); q.pop();
for (int v : adj[u]) {
if (dist[v] == -1) {
dist[v] = dist[u] + 1;
q.push(v);
}
}
}
for (int u = 1; u <= n; u++) {
cout << dist[u];
if (u < n) cout << " ";
}
cout << "\n";
return 0;
}
// Time: O(V + E), Space: O(V + E)
Problem 5.4.5 β All-Pairs with Floyd π‘ Medium Given N cities (N β€ 300) and M roads, answer Q queries: "Is city u reachable from city v within distance D?"
Hint
Run Floyd-Warshall to get all-pairs shortest paths in `O(NΒ³)`. Each query is then `O(1)`: check `dist[u][v] <= D`.Problem 5.4.6 β Dijkstra + Binary Search π΄ Hard A delivery drone can carry a maximum weight of W. There are N cities connected by roads, each road has a weight limit. Find the path from city 1 to city N that maximizes the minimum weight limit along the path (i.e., the heaviest cargo the drone can carry).
Hint
This is "Maximum Bottleneck Path" β find the path where the minimum edge weight is maximized. Two approaches: (1) Binary search on the answer W, then check if a path exists using only edges with weight β₯ W. (2) Run a modified Dijkstra where `dist[v]` = maximum minimum edge weight on any path to v. Use max-heap, update: `dist[v] = max(dist[v], min(dist[u], weight(u,v)))`.End of Chapter 5.4 β Next: Chapter 6.1: Introduction to DP