// STUHUB · C++ DATA STRUCTURES
Dijkstra, Prim, Kruskal and Topological Sort: Choosing the Right Graph Algorithm
The four weighted-graph algorithms people mix up, told apart on one seven-vertex graph: Dijkstra picks the smallest total distance, Prim picks the smallest single edge, Kruskal sorts every edge and merges a forest, and topological sort orders a DAG by in-degree. Includes a full relaxation trace, compiled C++ for all four, why BFS beats Dijkstra on an unweighted graph, and why a negative edge breaks Dijkstra.
Introduction
Four algorithms, one page of notes, and by the time the paper is in front of you they have blurred into a single fog of greedy-picks-the-smallest-thing. That is the real problem with this topic. Nobody fails it because Dijkstra is hard; people fail it because they run Prim's rule inside Dijkstra's table, or reach for Dijkstra on a graph with no weights on it at all.
So this page is built around the differences rather than the definitions. Every algorithm here runs on the same seven-vertex weighted graph, so when Dijkstra and Prim disagree you can see exactly which edge they disagree about and why. The short version, which is worth knowing cold: Dijkstra compares total distance back to the source; Prim compares one edge in isolation; Kruskal never grows anything, it merges; topological sort is not about weights at all.
Every listing below was compiled with g++ -std=c++17 -Wall -Wextra and run, and the tables in the text are the programs' actual output rather than a hand trace that might not survive contact with a compiler.
Four algorithms, four different questions
Before any mechanics, fix what each one is for. Almost every wrong answer in this topic is an algorithm applied to the wrong question, not an algorithm executed badly.
| Algorithm | The question it answers | Graph it needs | What comes out |
|---|---|---|---|
| Dijkstra | From this one vertex, what is the cheapest route to every other vertex? | Weighted, non-negative weights, directed or not | A distance per vertex, plus a shortest-path tree |
| Prim | What is the cheapest set of edges that keeps everything connected? | Weighted, connected, undirected | A minimum spanning tree |
| Kruskal | Same question as Prim, answered edge-first | Weighted, undirected (handles a disconnected graph as a forest) | A minimum spanning tree |
| Topological sort | In what order can I do these tasks without breaking a prerequisite? | Directed, acyclic, weights irrelevant | A linear ordering of the vertices |
Two consequences fall straight out of that table and are worth saying out loud.
First, Dijkstra and MST are not the same problem, even though both are greedy and both build a tree out of a weighted graph. A shortest-path tree minimises each individual distance from the source. A minimum spanning tree minimises the total weight of the whole tree. Those are different objectives and they generally produce different trees — you will see them differ by exactly one edge later on this page.
Second, Prim and Kruskal are the same problem. They are two strategies for one output. On a graph where all edge weights are distinct they return literally the same tree; where weights tie they may return different trees, but always with the same total weight.
Relaxation: the one idea Dijkstra is built from
Dijkstra keeps a single array, dist[], holding the best distance to each vertex known so far. Everything starts at infinity except the source, which is 0. These are guesses, and they only ever get smaller.
Relaxation is the act of improving one guess using one edge. Given an edge from u to v with weight w:
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w; // found a cheaper way in, through u
parent[v] = u; // remember how we got here
}That is the entire arithmetic content of the algorithm. Read it in English: if reaching u and then taking this edge is cheaper than whatever I currently believe about v, believe the cheaper thing instead. The name is mechanical — the estimate for v was stretched too tight, and the edge lets it relax down to a smaller value.
The loop around it is what makes the guesses final:
- Among the vertices not yet settled, take the one with the smallest current
dist. - Declare it settled. Its distance will never improve again.
- Relax every edge leaving it.
- Repeat until every vertex is settled.
Step 2 is the part that needs an argument, and the argument is the reason negative weights are banned. When you pick the smallest unsettled dist, any other route to that vertex would have to go through some other unsettled vertex first — one whose distance is already greater or equal — and then travel further along at least one more edge. With non-negative weights, travelling further can never make the total smaller, so no such route can win. Allow one negative edge and that sentence collapses: a longer prefix can be redeemed by a negative edge later, and a vertex you already froze turns out to have been wrong.
A useful mental separation while tracing by hand: settling a vertex is a decision you never revisit; relaxing an edge is a guess you may overwrite several times. In the trace below, vertex D is relaxed twice — 12 first, then 8 — before it is ever settled.
The graph everything on this page runs on
Seven vertices, A to G, ten undirected weighted edges:
A -- B (2) A -- C (3) B -- C (2) B -- D (10) C -- E (4)
D -- E (1) D -- F (6) E -- F (3) E -- G (8) F -- G (2)As an adjacency list, which is how the code stores it:
| Vertex | Neighbours (weight) |
|---|---|
| A | B (2), C (3) |
| B | A (2), C (2), D (10) |
| C | A (3), B (2), E (4) |
| D | B (10), E (1), F (6) |
| E | C (4), D (1), F (3), G (8) |
| F | D (6), E (3), G (2) |
| G | E (8), F (2) |
Two features of this graph are deliberate. The edge B–D (10) is a trap: it is the only direct route into D from the left-hand side of the graph and it is expensive, so D is first relaxed to a bad value and later rescued from the other direction. And the triangle A–B (2), B–C (2), A–C (3) is what will make the shortest-path tree and the minimum spanning tree disagree.
Dijkstra traced from A, one settled vertex per row
Source A. Each row is one iteration: the vertex settled on the left with its final distance, and the state of the whole dist array after that vertex has relaxed all of its edges.
| settled | A | B | C | D | E | F | G |
|---|---|---|---|---|---|---|---|
| (start) | 0 | inf | inf | inf | inf | inf | inf |
| A (0) | 0 | 2 | 3 | inf | inf | inf | inf |
| B (2) | 0 | 2 | 3 | 12 | inf | inf | inf |
| C (3) | 0 | 2 | 3 | 12 | 7 | inf | inf |
| E (7) | 0 | 2 | 3 | 8 | 7 | 10 | 15 |
| D (8) | 0 | 2 | 3 | 8 | 7 | 10 | 15 |
| F (10) | 0 | 2 | 3 | 8 | 7 | 10 | 12 |
| G (12) | 0 | 2 | 3 | 8 | 7 | 10 | 12 |
Walk the interesting rows.
- Settling B, not C. After A relaxes its two edges, B is at 2 and C is at 3, so B goes first. B then offers C a route of 2 + 2 = 4, which is worse than the 3 C already has, so nothing changes. This is the most commonly mis-traced moment on any Dijkstra paper: a relaxation that fails is still a step, and the failed comparison is exactly what you must write down to show your working.
- D relaxed to 12, then to 8. B is D's only neighbour early on, and B–D costs 10, so D sits at 12. Four rows later E is settled at 7 and the edge E–D (1) drops D to 8. A vertex's estimate improving after several rounds is normal; what would be a bug is an estimate improving after the vertex was settled.
- D is settled but changes nothing. When D's turn comes at 8, its edge to F offers 8 + 6 = 14, worse than F's existing 10. Some rows do no work. That is not a mistake in your trace.
- G relaxed twice. E gives G 15 via the expensive E–G (8); F later gives it 12. G is settled last, at 12.
Final answers, with the path reconstructed by walking the parent array backwards:
vertex dist path
A 0 A
B 2 A-B
C 3 A-C
D 8 A-C-E-D
E 7 A-C-E
F 10 A-C-E-F
G 12 A-C-E-F-GNote that the cheapest route to D does not use the only edge that touches D from the source's side of the graph in one hop. Greedy on the frontier, not greedy on the map.
Dijkstra in C++ with a priority queue
The program below is the source of the table above — it prints that table and then the path list, verbatim.
Two implementation points are worth more than the rest of the code.
Choosing the next vertex. The textbook version scans the whole dist array for the smallest unsettled entry, which is O(V) per round and O(V²) overall — perfectly fine for exam-sized graphs and easier to trace. The version here uses std::priority_queue with std::greater to turn the default max-heap into a min-heap, which gets you O((V + E) log V).
Stale heap entries. A priority queue has no decrease-key operation, so when a vertex is relaxed a second time you push a new pair rather than editing the old one. The old, worse pair is still sitting in the heap. The line if (done[u]) continue; is what discards it. Leave that line out and you will re-expand vertices, and on some graphs relax edges out of a vertex using a distance that is no longer current. In the trace above, D is pushed at 12 and again at 8; the 12 is popped later and thrown away.
INF is std::numeric_limits<int>::max(), so the guard dist[u] != INF before the addition matters: without it, INF + w overflows signed int, which is undefined behaviour, and in practice wraps to a large negative number that looks like a wonderfully short path.
#include <iostream>
#include <iomanip>
#include <limits>
#include <queue>
#include <string>
#include <vector>
const int INF = std::numeric_limits<int>::max();
struct Edge { int to; int w; };
// Undirected weighted graph, vertices 0..6 == A..G.
const std::string NAME = "ABCDEFG";
void addEdge(std::vector<std::vector<Edge>>& g, int u, int v, int w) {
g[u].push_back({v, w});
g[v].push_back({u, w});
}
void printRow(const std::string& label, const std::vector<int>& dist) {
std::cout << std::left << std::setw(14) << label;
for (std::size_t v = 0; v < dist.size(); ++v) {
if (dist[v] == INF) std::cout << std::right << std::setw(4) << "inf";
else std::cout << std::right << std::setw(4) << dist[v];
}
std::cout << '\n';
}
int main() {
const int V = 7;
std::vector<std::vector<Edge>> g(V);
addEdge(g, 0, 1, 2); // A-B 2
addEdge(g, 0, 2, 3); // A-C 3
addEdge(g, 1, 2, 2); // B-C 2
addEdge(g, 1, 3, 10); // B-D 10
addEdge(g, 2, 4, 4); // C-E 4
addEdge(g, 3, 4, 1); // D-E 1
addEdge(g, 3, 5, 6); // D-F 6
addEdge(g, 4, 5, 3); // E-F 3
addEdge(g, 4, 6, 8); // E-G 8
addEdge(g, 5, 6, 2); // F-G 2
const int src = 0;
std::vector<int> dist(V, INF);
std::vector<int> parent(V, -1);
std::vector<bool> done(V, false);
dist[src] = 0;
// (distance, vertex); greater<> turns the max-heap into a min-heap.
std::priority_queue<std::pair<int, int>,
std::vector<std::pair<int, int>>,
std::greater<std::pair<int, int>>> pq;
pq.push({0, src});
std::cout << std::left << std::setw(14) << "settled";
for (int v = 0; v < V; ++v) std::cout << std::right << std::setw(4) << NAME[v];
std::cout << '\n';
printRow("(start)", dist);
while (!pq.empty()) {
auto [d, u] = pq.top();
pq.pop();
if (done[u]) continue; // stale heap entry: a better one was processed already
done[u] = true;
for (const Edge& e : g[u]) {
if (dist[u] != INF && dist[u] + e.w < dist[e.to]) {
dist[e.to] = dist[u] + e.w; // relaxation
parent[e.to] = u;
pq.push({dist[e.to], e.to});
}
}
printRow(std::string(1, NAME[u]) + " (" + std::to_string(d) + ")", dist);
}
std::cout << "\nvertex dist path\n";
for (int v = 0; v < V; ++v) {
std::string path(1, NAME[v]);
for (int p = parent[v]; p != -1; p = parent[p]) path = NAME[p] + ("-" + path);
std::cout << " " << NAME[v] << std::right << std::setw(6) << dist[v]
<< " " << path << '\n';
}
return 0;
}Minimum spanning trees: Kruskal merges a forest, Prim grows one tree
A spanning tree of a connected graph touches every vertex using the fewest edges that can do it: exactly V − 1, and no cycles. A minimum spanning tree is the spanning tree whose weights sum to the least. The V − 1 count is not a rule of thumb, it is a definition you can check your answer against — V − 2 edges cannot reach everything, V edges must contain a cycle. On our seven-vertex graph, any correct answer has six edges.
Kruskal ignores connectivity until the last moment. Sort all ten edges by weight, then walk the sorted list and take each edge unless its two endpoints are already connected to each other. Early on you are building several disconnected fragments — a forest — and the algorithm's job is to merge them. Cycle detection is done by union-find: find returns which fragment a vertex belongs to, unite merges two fragments and returns false if they were already the same one.
Kruskal (whole edge list, cheapest first)
D-E (1) take
A-B (2) take
B-C (2) take
F-G (2) take
A-C (3) skip: would close a cycle
E-F (3) take
C-E (4) take
D-F (6) skip: would close a cycle
E-G (8) skip: would close a cycle
B-D (10) skip: would close a cycle
edges chosen: 6 (V-1 = 6), total weight 14Read the order it accepted things: D–E, then A–B, then F–G. At that point Kruskal is holding three separate fragments that have nothing to do with each other. It is not building a tree, it is building a forest and merging it, and only the very last accepted edge (C–E) makes the thing connected.
Prim never has more than one fragment. Start at any vertex; repeatedly add the cheapest edge with one end inside the tree and one end outside; stop after V − 1 additions.
Prim (one tree, grown from A)
add A-B (2)
add B-C (2)
add C-E (4)
add E-D (1)
add E-F (3)
add F-G (2)
total weight 14Same six edges. Completely different order. Prim took C–E (4) as its third edge because at that moment 4 was the cheapest way out of the tree {A, B, C}; Kruskal took it last, after every cheaper edge in the graph had been considered. Prim added D–E (1) fourth; Kruskal added it first, before it knew anything about the rest of the graph. If a question asks for the order in which edges are chosen, the two algorithms give different — both correct — answers, and that is exactly what such a question is testing.
One more contrast that decides which one to implement: Kruskal needs the whole edge list up front and costs O(E log E) dominated by the sort, which suits sparse graphs. Prim only ever looks at the frontier and, in the simple array form below, costs O(V²) regardless of how many edges there are, which suits dense ones.
The program below produces both traces.
#include <algorithm>
#include <iostream>
#include <limits>
#include <string>
#include <vector>
const int INF = std::numeric_limits<int>::max();
const std::string NAME = "ABCDEFG";
const int V = 7;
struct Edge { int u; int v; int w; };
const std::vector<Edge> EDGES = {
{0, 1, 2}, {0, 2, 3}, {1, 2, 2}, {1, 3, 10}, {2, 4, 4},
{3, 4, 1}, {3, 5, 6}, {4, 5, 3}, {4, 6, 8}, {5, 6, 2},
};
// ---- union-find (disjoint set) -------------------------------------------
struct DSU {
std::vector<int> parent, rank_;
explicit DSU(int n) : parent(n), rank_(n, 0) {
for (int i = 0; i < n; ++i) parent[i] = i;
}
int find(int x) { // path compression
return parent[x] == x ? x : parent[x] = find(parent[x]);
}
bool unite(int a, int b) { // false == already connected
a = find(a); b = find(b);
if (a == b) return false;
if (rank_[a] < rank_[b]) std::swap(a, b);
parent[b] = a;
if (rank_[a] == rank_[b]) ++rank_[a];
return true;
}
};
void kruskal() {
std::vector<Edge> es = EDGES;
std::sort(es.begin(), es.end(),
[](const Edge& a, const Edge& b) { return a.w < b.w; });
DSU dsu(V);
int total = 0, chosen = 0;
std::cout << "Kruskal (whole edge list, cheapest first)\n";
for (const Edge& e : es) {
bool taken = dsu.unite(e.u, e.v); // false: both ends already connected
std::cout << " " << NAME[e.u] << '-' << NAME[e.v] << " (" << e.w << ") "
<< (taken ? "take" : "skip: would close a cycle") << '\n';
if (taken) { total += e.w; ++chosen; }
}
std::cout << " edges chosen: " << chosen << " (V-1 = " << V - 1
<< "), total weight " << total << "\n\n";
}
void prim(int start) {
std::vector<int> best(V, INF); // cheapest edge joining v to the tree
std::vector<int> from(V, -1);
std::vector<bool> inTree(V, false);
std::vector<std::vector<int>> w(V, std::vector<int>(V, INF));
for (const Edge& e : EDGES) { w[e.u][e.v] = e.w; w[e.v][e.u] = e.w; }
best[start] = 0;
int total = 0;
std::cout << "Prim (one tree, grown from " << NAME[start] << ")\n";
for (int step = 0; step < V; ++step) {
int u = -1;
for (int v = 0; v < V; ++v)
if (!inTree[v] && best[v] != INF && (u == -1 || best[v] < best[u])) u = v;
if (u == -1) { std::cout << " graph is disconnected\n"; return; }
inTree[u] = true;
if (from[u] != -1) {
total += best[u];
std::cout << " add " << NAME[from[u]] << '-' << NAME[u]
<< " (" << best[u] << ")\n";
}
for (int v = 0; v < V; ++v)
if (!inTree[v] && w[u][v] < best[v]) { best[v] = w[u][v]; from[v] = u; }
}
std::cout << " total weight " << total << '\n';
}
int main() {
kruskal();
prim(0);
return 0;
}Dijkstra versus Prim: the smallest total against the smallest edge
Both algorithms run the same outer loop — pick the cheapest thing on the frontier, absorb it, update the frontier — and the only difference is what cheapest measures.
| Dijkstra | Prim | |
|---|---|---|
| Key stored per vertex | dist[v] = total weight of the whole path from the source | best[v] = weight of the single cheapest edge joining v to the tree |
| Update rule | dist[u] + w < dist[v] | w < best[v] |
| Objective | every individual distance from the source is minimal | the sum over the tree is minimal |
| Source matters? | yes, answers are relative to it | no, the starting vertex changes the order, never the tree |
That single missing dist[u] + is the whole difference, and it is the single most valuable line to have memorised on this topic.
Now watch it bite on our graph. Dijkstra from A produced this tree, reading the parent array: A–B (2), A–C (3), C–E (4), E–D (1), E–F (3), F–G (2) — total 15. The MST is A–B (2), B–C (2), C–E (4), D–E (1), E–F (3), F–G (2) — total 14.
They differ in exactly one edge. Both trees have to attach C somehow, and there are two candidates:
- Dijkstra keeps A–C (3). It is not comparing 3 against 2. It is comparing the route A–C (3) against the route A–B–C (2 + 2 = 4), and 3 wins. Keeping B–C would make C's distance from A equal to 4 — a worse answer to the question Dijkstra was asked.
- Prim and Kruskal keep B–C (2). They do not care how far C ends up from A. Once B is attached, joining C costs 2 this way and 3 the other way, so 2 wins, and the total drops from 15 to 14.
And the price the MST pays: in the minimum spanning tree, the only route from A to C is A–B–C, length 4, when the graph plainly contains a direct edge of length 3. A minimum spanning tree does not preserve shortest paths. If someone asks for the cheapest cabling to connect every building, that is fine. If they ask for the fastest route from the depot, it is the wrong tree.
A sanity check that costs five seconds and catches most confusion between the two: an MST answer is a set of edges with a total, and the set has exactly V − 1 members. A Dijkstra answer is a number per vertex. If your Dijkstra answer is a set of edges with a total, or your MST answer is a table of distances from a source, you have run the wrong algorithm.
Topological sort: order by in-degree, and get cycle detection for free
Different problem entirely. No weights, and the graph must be directed and acyclic (a DAG). An edge u → v means u must come before v. A topological order is any listing of the vertices that respects every such edge at once. Build systems, course prerequisites, spreadsheet recalculation and task schedulers all run on this.
The in-degree method — Kahn's algorithm — is the one to hand-trace, because its state is a single array of counters:
- Compute the in-degree of every vertex: how many prerequisites it still has.
- Put every vertex with in-degree 0 into a queue. These are the tasks that can start immediately.
- Remove one from the queue, append it to the output, and decrement the in-degree of each of its successors. Any successor that drops to 0 has just had its last prerequisite satisfied — enqueue it.
- Repeat until the queue is empty.
Take this dependency DAG on the same seven letters: A → C, B → C, C → D, C → E, D → F, E → F, B → G, G → E. Starting in-degrees are A 0, B 0, C 2, D 1, E 2, F 2, G 1, so the queue starts as [A, B].
| removed | A | B | C | D | E | F | G |
|---|---|---|---|---|---|---|---|
| A | - | 0 | 1 | 1 | 2 | 2 | 1 |
| B | - | - | 0 | 1 | 2 | 2 | 0 |
| C | - | - | - | 0 | 1 | 2 | 0 |
| G | - | - | - | 0 | 0 | 2 | - |
| D | - | - | - | - | 0 | 1 | - |
| E | - | - | - | - | - | 0 | - |
| F | - | - | - | - | - | - | - |
A dash means the vertex has already been output. The result is A B C G D E F.
Three things to take from that trace.
The answer is not unique. A B C G D E F is one of twelve valid orderings of this DAG. Swap the queue for a stack and you get a different, equally correct one. Never write the topological order; write a topological order, and be ready to justify it edge by edge if a marker asks.
E waits for two different predecessors. It is reachable from C and from G, and it only becomes available on the fourth removal, when G finally clears its second dependency. The in-degree counter is doing exactly the bookkeeping you would otherwise do wrong in your head.
Cycle detection is free. If the loop ends with fewer than V vertices in the output, the leftovers are stuck in a cycle — each one is waiting on another one that is also waiting. Add F → C to this DAG and the program stops after emitting A, B and G: everything downstream is now waiting on itself. That check is one comparison and it is the only correct way to answer does a topological order exist.
Complexity is O(V + E): each vertex enters and leaves the queue once, and each edge is decremented once.
#include <iostream>
#include <queue>
#include <string>
#include <vector>
const std::string NAME = "ABCDEFG";
const int V = 7;
// Directed edges of a build-dependency DAG: u must come before v.
const std::vector<std::pair<int, int>> EDGES = {
{0, 2}, {1, 2}, {2, 3}, {2, 4}, {3, 5}, {4, 5}, {1, 6}, {6, 4},
};
int main() {
std::vector<std::vector<int>> succ(V);
std::vector<int> indeg(V, 0);
for (auto [u, v] : EDGES) { succ[u].push_back(v); ++indeg[v]; }
std::queue<int> ready;
for (int v = 0; v < V; ++v) if (indeg[v] == 0) ready.push(v);
std::vector<int> order;
std::cout << "removed in-degrees after removal (A..G)\n";
while (!ready.empty()) {
int u = ready.front();
ready.pop();
order.push_back(u);
for (int v : succ[u]) if (--indeg[v] == 0) ready.push(v);
std::cout << " " << NAME[u] << " ";
for (int v = 0; v < V; ++v) {
bool out = false;
for (int done : order) if (done == v) out = true;
std::cout << (out ? " -" : " " + std::to_string(indeg[v]));
}
std::cout << '\n';
}
if (static_cast<int>(order.size()) != V) {
std::cout << "\ncycle detected: only " << order.size() << " of " << V
<< " vertices ever reached in-degree 0\n";
return 0;
}
std::cout << "\ntopological order: ";
for (int v : order) std::cout << NAME[v] << ' ';
std::cout << '\n';
return 0;
}Three situations where Dijkstra is the wrong tool
The graph has no weights. Then BFS already solves it. On an unweighted graph every edge costs 1, so the number of edges on a path is its length, and BFS visits vertices in exactly non-decreasing order of that count — it settles the whole distance-1 layer, then the whole distance-2 layer, and so on. Running Dijkstra here is not wrong in the sense of giving a wrong answer; it gives the same answer while paying for a priority queue you did not need, turning O(V + E) into O((V + E) log V). If a question hands you an unweighted graph and asks for a shortest path, the expected answer is BFS, and reaching for Dijkstra suggests you have not noticed the weights are missing.
The converse trap is worse: running BFS on a weighted graph and reporting the fewest-hops path as the shortest. On the graph above, A–B–D is two hops and costs 12; A–C–E–D is three hops and costs 8. Fewest edges is not cheapest.
Some weight is negative. Dijkstra's freeze-the-smallest step assumes going further can never get cheaper, and a negative edge destroys that assumption. The failure is quiet — you get a number, it is just wrong. Bellman-Ford is the replacement: instead of settling one vertex per round it relaxes every edge, V − 1 times over, at O(V · E). It costs more and it buys two things: correctness with negative edges, and detection of a negative cycle (if a V-th pass still improves something, there is a loop you can go round forever getting cheaper, and shortest path stops being a meaningful question).
You need every pair, not one source. Running Dijkstra from all V sources works and is often the right practical choice on a sparse graph. On a dense one, Floyd-Warshall is simpler and competitive: three nested loops over dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]), O(V³), handling negative edges as long as there is no negative cycle. Its middle-out logic — k is allowed as an intermediate vertex now — is worth understanding once, because the three-line body is easy to reproduce from scratch and hard to reconstruct from a half-memory.
Complexity, and how to choose under time pressure
| Algorithm | Typical complexity | Supporting structure |
|---|---|---|
| DFS / BFS | O(V + E) | stack (DFS) / queue (BFS) |
| Dijkstra, array scan | O(V²) | dist array plus a settled flag |
| Dijkstra, heap | O((V + E) log V) | min-priority queue |
| Bellman-Ford | O(V · E) | edge list |
| Floyd-Warshall | O(V³) | V × V matrix |
| Kruskal | O(E log E) | sorted edge list plus union-find |
| Prim, array scan | O(V²) | key array |
| Prim, heap | O(E log V) | min-priority queue |
| Topological sort | O(V + E) | in-degree array plus a queue |
Two readings of that table that matter more than memorising the rows. Dijkstra and Prim appear twice each for the same reason: both repeatedly ask what is the smallest key still outstanding, and you either scan an array for it, O(V) per round, or keep a heap, O(log V) per push. So the dense choice is O(V²) and the sparse choice is the log one. And notice Kruskal's cost is dominated by the sort, not by the union-find, which is near-constant per operation in practice.
Matching the requirement to the algorithm, phrased the way questions phrase it:
| If you are asked for… | Reach for |
|---|---|
| Cheapest route from one place to everywhere, weights ≥ 0 | Dijkstra |
| Fewest hops on an unweighted graph | BFS |
| Cheapest route where some weights are negative | Bellman-Ford |
| Cheapest route between every pair | Floyd-Warshall, or Dijkstra from each vertex |
| Cheapest cabling / piping / roads connecting everything | MST — Prim or Kruskal |
| MST on a dense graph, or growth from a given start point | Prim |
| MST on a sparse graph, or the edge list is already sorted | Kruskal |
| An order for tasks with prerequisites | Topological sort |
| Whether a directed graph contains a cycle | Topological sort, and check the output length |
| Just reach everything / test connectivity | DFS or BFS |
One keyword heuristic that survives most phrasings: route, journey, from-here-to-there → shortest path. Connect everything, network, cable, total cost → spanning tree. Before, after, depends on, prerequisite → topological sort.
The mistakes people actually make
- Running Prim's update rule inside a Dijkstra table. Writing
w < dist[v]instead ofdist[u] + w < dist[v]. The table fills in, every number looks plausible, and the answer is an MST-shaped object that answers nobody's question. If your Dijkstra distances are all small edge weights rather than growing totals, this is what happened. - Settling the vertex you just relaxed rather than the global minimum. Dijkstra settles the smallest key anywhere on the frontier, which is frequently not a neighbour of the vertex you just processed. On the graph above, after settling C at 3 the next vertex is E at 7 — not D, even though D was the thing most recently touched.
- Not recording failed relaxations.
2 + 2 = 4, not less than3, so C keeps 3. That comparison is a step of the algorithm and, in a hand trace, it is the evidence that you did the step. - Believing a distance can improve after settling. It cannot, with non-negative weights. If your trace shows a settled vertex changing, you either settled the wrong vertex or the graph has a negative edge and Dijkstra was never applicable.
- Integer overflow on infinity.
dist[u] + wwheredist[u]isINT_MAXis undefined behaviour and typically wraps negative, producing spectacular fake shortcuts. Guard it, or use anINFlike1e9that has headroom for one addition. - Forgetting the stale-entry check in the heap version. No
decrease-keymeans duplicates in the queue. Withoutif (done[u]) continue;you re-expand vertices with out-of-date distances. - Producing an MST with the wrong number of edges. It is exactly V − 1. Fewer and it does not span; more and it has a cycle. Count before you write the total.
- Doing Kruskal's cycle check by eye. On a small hand-drawn graph you can get away with it; in code, comparing whether two vertices are already connected is precisely what union-find is for, and a
visitedarray does not do the same job — a fragment is not a vertex. - Assuming Prim and Kruskal must select edges in the same order. They select the same set (when weights are distinct) in almost always different sequences. Answer the question that was asked: set, total, or order.
- Using Dijkstra's tree as a spanning tree, or an MST as a route map. Different objectives, demonstrated above: 15 against 14, and A-to-C is 3 in one tree and 4 in the other.
- Topologically sorting an undirected graph. Meaningless — an undirected edge imposes no order — and any undirected edge is trivially a cycle, so no order exists. If the question says undirected, it is not asking for a topological sort.
- Reporting a partial topological order as the answer. If the algorithm outputs fewer than V vertices, the correct answer is no topological order exists, the graph has a cycle, not the prefix it managed to emit.
Exercises with solutions
Work through each question before opening the solution below it.
Exercise 1
Trace Dijkstra from a different source. Using the same seven-vertex graph (A–B 2, A–C 3, B–C 2, B–D 10, C–E 4, D–E 1, D–F 6, E–F 3, E–G 8, F–G 2), run Dijkstra from G. Give the settle order, the final distance to every vertex, and the shortest path to A. Then answer two questions without re-running anything: does the shortest-path tree from G differ from the one rooted at A, and does the minimum spanning tree change?
Solution
Initialise dist[G] = 0, everything else infinite, then settle the smallest unsettled key each round.
| settled | A | B | C | D | E | F | G |
|---|---|---|---|---|---|---|---|
| (start) | inf | inf | inf | inf | inf | inf | 0 |
| G (0) | inf | inf | inf | inf | 8 | 2 | 0 |
| F (2) | inf | inf | inf | 8 | 5 | 2 | 0 |
| E (5) | inf | inf | 9 | 6 | 5 | 2 | 0 |
| D (6) | inf | 16 | 9 | 6 | 5 | 2 | 0 |
| C (9) | 12 | 11 | 9 | 6 | 5 | 2 | 0 |
| B (11) | 12 | 11 | 9 | 6 | 5 | 2 | 0 |
| A (12) | 12 | 11 | 9 | 6 | 5 | 2 | 0 |
Settle order G, F, E, D, C, B, A; distances A 12, B 11, C 9, D 6, E 5, F 2, G 0.
The rows worth defending. G's own edge to E costs 8, but F reaches E for 2 + 3 = 5, so E is relaxed down before it is ever settled — the direct edge from the source is not always the way in. D is offered 8 by F (2 + 6) and then 6 by E (5 + 1). B is first offered 16 through D's expensive B–D (10) and then 11 through C, another two-stage relaxation. And when B is finally settled at 11 it offers A 11 + 2 = 13, which loses to the 12 A already holds via C.
Shortest path to A: G–F–E–C–A, total 2 + 3 + 4 + 3 = 12. Note it uses A–C (3) rather than A–B (2) plus B–C (2), the same edge choice Dijkstra made from the other end.
Does the tree change? Yes. From A the tree was A–B, A–C, C–E, E–D, E–F, F–G. From G it is G–F, F–E, E–D, E–C, C–B, C–A. Four edges are shared; B now attaches through C rather than through A. A shortest-path tree is defined relative to its source, so there is no reason for the two to agree, and here they do not.
Does the MST change? No, and it cannot. A minimum spanning tree is a property of the graph alone — no source appears anywhere in its definition. Prim takes a starting vertex only to decide the order in which it discovers edges; run prim(6) instead of prim(0) and you get the same six edges, the same total of 14, in a different sequence. This is the cleanest way to tell the two problems apart: change the source and Dijkstra's answer changes, while the MST does not move.
Exercise 2
Ties, and why V − 1 is not negotiable. Consider four vertices W, X, Y, Z with four edges, all of weight 1, forming a square: W–X, X–Y, Y–Z, Z–W. (a) How many distinct minimum spanning trees does this graph have, and what is the total weight of each? (b) Which one does Kruskal produce if the sorted edge list happens to be in the order written above, and which does Prim produce starting from Y? (c) Now argue in general why a spanning tree of a connected graph with V vertices must have exactly V − 1 edges — both why fewer is impossible and why more is.
Solution
(a) Four MSTs, each of total weight 3. A spanning tree here needs 4 − 1 = 3 edges, and any 3 of the 4 edges work: dropping exactly one edge from a 4-cycle leaves a path through all four vertices, which is a tree. There are four ways to choose which edge to drop, so there are four spanning trees, and since every edge weighs 1 they all total 3 — all four are minimum. Ties in the weights are precisely when the MST stops being unique. (If all edge weights are distinct, the MST is unique, and then Prim and Kruskal are guaranteed to return the same tree.)
(b) Kruskal walks the sorted list and takes anything that does not close a cycle. With ties, sorted does not pin down an order, so whatever order the sort produces is what you get. Taking them as written: W–X take, X–Y take, Y–Z take — that is already 3 edges spanning all four vertices — then Z–W is skipped because W and Z are now in the same fragment. Kruskal's tree: W–X, X–Y, Y–Z.
Prim from Y sees two edges out of the tree, X–Y and Y–Z, both weight 1, and takes whichever its tie-break prefers; say X–Y. The tree is {X, Y}; the frontier is W–X (1) and Y–Z (1); take Y–Z, giving {X, Y, Z}; now only Z–W (1) and W–X (1) reach W, and taking Z–W gives X–Y, Y–Z, Z–W — a different tree from Kruskal's, of the same total weight 3. Both are correct. A tied graph simply has no single right answer, which is why you should state the tie-break you used rather than presenting one tree as the MST.
(c) Two halves.
Fewer than V − 1 is impossible. Start with V vertices and no edges: V separate components. Adding one edge can reduce the component count by at most one — it can only ever merge the two components its endpoints are in, and if they were already the same component it merges nothing. To get from V components down to 1 you therefore need at least V − 1 edges. With fewer, some vertex is unreachable and the subgraph does not span.
More than V − 1 is impossible for a tree. Suppose you have a connected subgraph with V vertices and V edges or more. Build it up one edge at a time: each of the first V − 1 edges can reduce the component count by at most one, so by the time you have added V − 1 edges and reached one component, every subsequent edge joins two vertices that are already connected. That edge plus the existing path between its endpoints forms a cycle, and a tree by definition has none.
So connected plus acyclic pins the count to exactly V − 1. Practically: count the edges in your answer before you total the weights. It is the fastest error check on this whole topic, and it catches both the dropped edge and the accidentally accepted cycle.
Exercise 3
Prerequisites, orders and cycles. Take the DAG A → C, B → C, C → D, C → E, D → F, E → F, B → G, G → E. (a) Kahn's algorithm with a queue gave A B C G D E F. What does the same algorithm give if you use a stack instead — always removing the most recently available vertex — and is it valid? (b) Which vertex must come last in every valid ordering, and why is neither A nor B forced to come first? (c) Add the edge F → C. Trace what the in-degree algorithm now does and state precisely what the output means.
Solution
Starting in-degrees: A 0, B 0, C 2, D 1, E 2, F 2, G 1.
(a) With a stack. Push the in-degree-0 vertices A and B; the stack holds [A, B] with B on top.
- Pop B. Decrement C to 1 and G to 0; push G. Stack [A, G].
- Pop G. Decrement E to 1. Stack [A].
- Pop A. Decrement C to 0; push C. Stack [C].
- Pop C. Decrement D to 0 and E to 0; push both. Stack [D, E].
- Pop E. Decrement F to 1. Stack [D].
- Pop D. Decrement F to 0; push F. Stack [F].
- Pop F.
Order B G A C E D F. It is valid — check every edge: A→C (A 3rd, C 4th), B→C, C→D, C→E, D→F, E→F, B→G, G→E all run left to right. The container decides which valid order you get, never whether the order is valid; the algorithm's correctness rests only on never removing a vertex whose in-degree is above 0. This DAG has twelve valid orderings in total, so any question asking for one should be answered as a topological order, with the edge check shown.
(b) F must be last. Every other vertex has a path to F: D→F and E→F directly, C through both of them, A and B through C, and G through E. A vertex with a path into it from everything else cannot be placed before any of them. Equivalently, F is the only vertex with out-degree 0, and the last position must be a vertex with no outgoing edges.
First place is different. A and B are the only in-degree-0 vertices, so the first vertex must be one of them, but nothing orders A relative to B — there is no path either way. Both A-first and B-first orderings exist, which is exactly what part (a) demonstrates. Forced last and forced first are not symmetric here: F is unique, the source is not.
(c) Add F → C and the graph is no longer a DAG. In-degrees become A 0, B 0, C 3, D 1, E 2, F 2, G 1. The queue starts [A, B].
- Remove A: C drops to 2.
- Remove B: C drops to 1, G drops to 0 and is enqueued.
- Remove G: E drops to 1.
- Queue is now empty — but only three vertices have been output.
C still needs one more predecessor (F), F needs D and E, D needs C, E needs C. Each is waiting on another member of the cycle C → D → F → C, so none of them will ever reach in-degree 0. The output is not a partial answer to be written down as A B G and then the rest; the correct statement is: no topological order exists, because the graph contains a cycle, and the four vertices never emitted — C, D, E, F — are the ones on or downstream of it. In code the test is one comparison, order.size() != V, and it is the standard way to answer is this directed graph acyclic.
Published and updated 2026-08-09 · DUOCODE TECHNOLOGY