Two parameters

Counting on a graph

An instrumented array counts comparisons, swaps, reads and writes, and none of those is what a graph algorithm spends its time on. Three new primitives are needed — an adjacency scanned, a vertex first reached, an edge relaxed — and once they exist, breadth-first and depth-first search turn out to be the same algorithm by every count kept on arrays.

Every measurement on this site so far has gone through an instrumented array whose four primitives — read, write, compare, swap — tally exactly. That instrument is the reason a caption here can quote a number instead of a feeling, and it does not generalise to a graph at all.

Not “generalises badly”. Does not generalise. A breadth-first search over a graph held as adjacency lists performs no comparisons whatsoever, and if it is counted with the array’s instrument the answer is zero. The instrument is not measuring the wrong thing; it is measuring a thing that is not happening.

So the field needs its own primitives, and choosing them is the work.

Counted work against V, sparse, fixed average degreeEvery counted operation, summed, on logarithmic axes. The lines fan out because the algorithms differ in class rather than only in constant — at V = 64 the spread between best and worst is 108.1× and at V = 2048 it is 3509×. On this sweep E is proportional to V, so V, E and V + E are the same line and the picture cannot tell them apart.10010³10³10⁴10⁵10⁶10⁷Vcounted workBreadth-firstDijkstra, binary heapDijkstra, all V queuedBellman–Ford, all passesV from 64 to 2048, sparse, fixed average degreework = scans + visits + relaxations + queue comparisons
Fig. 1 Four graph algorithms on the same family of graphs, counted work against the number of vertices, on logarithmic axes. The lines fan out because these algorithms differ in class and not only in constant. Every unit in this picture is one of the four primitives described below, and the caption strip says which.

Three operations, and why those three

A graph algorithm spends its life doing three things, and each of them is the term that puts one of the two size parameters into the bound.

Scanning an adjacency. Asking a vertex which vertices it is joined to. Charged one unit per edge slot examined — so a vertex of degree seven costs seven, and a vertex of degree zero costs nothing. Summed over a traversal that visits everything, this is where the EE in every graph bound comes from.

Reaching a vertex for the first time. Charged once, and only once: calling visit on a vertex already seen returns false and costs nothing, so an algorithm cannot inflate its own count by asking twice. This is where the VV comes from.

Relaxing an edge. Attempting to improve a tentative distance — comparing what is known about a vertex against a route through one of its neighbours. Charged whether or not it improves anything, because the work is in the attempt. Dijkstra and Bellman–Ford are, in a real sense, nothing but sequences of relaxations.

And then a fourth quantity, counted separately rather than mixed in:

Priority-queue operations. Pushes, pops, decrease-keys, and the comparisons performed inside the queue. Kept apart because which queue an implementation uses decides its complexity class, the choice is invisible in every published pseudocode, and folding the queue’s work into the traversal’s would hide the one decision that matters most.

The unit, stated because it is a choice

These figures report a quantity called work: the four counts, summed. One edge slot examined, one vertex reached, one relaxation attempted and one queue comparison each count as one.

That is a cost model rather than a fact, and it is exactly the same kind of choice as charging one unit per comparison in the sorting field. A relaxation involves an addition and a comparison; an adjacency scan is a pointer dereference; a queue comparison is a comparison. They are not the same amount of machine. Summing them is defensible for comparing algorithms and indefensible as a prediction of anything, and both halves of that are printed on every figure that uses it.

The components are always kept separately, so any figure that wants one of them alone can have it — and two of them do, because the interesting story is usually about which component dominates rather than about the total.

The check that makes the counts mean anything

The sorting field’s central safeguard is that a sort which does not sort is not measured, it is an error, because a broken algorithm’s counts still plot a perfectly convincing curve. The graph field needs the same thing and the check is different in each case.

A traversal must reach every vertex of a connected graph. All the input graphs here are built connected — a random spanning tree first, then the extra edges — so a traversal that visits 2,047 of 2,048 has a bug, and a bug that would otherwise produce a beautifully linear curve.

Two shortest-path algorithms that share no code must agree. Dijkstra with a heap, Dijkstra with an array queue, and Bellman–Ford compute the same distances by three different routes. Requiring all three to agree at every vertex is a much stronger check than any of them passing a test alone, and it is the graph analogue of computing the sorting floor twice.

Prim and Kruskal must find trees of the same weight. Not the same tree — ties break differently — but the same total, because a minimum is a minimum. Two algorithms with nothing in common agreeing on a number is the strongest evidence available here.

And, as everywhere on this site, the checks are themselves checked: the gate hands the framework a traversal that deliberately stops one vertex early and requires it to be caught.

The first result: BFS and DFS are the same algorithm

With the instrument built, the most basic question in the field has an answer that is slightly startling.

Breadth-first and depth-first search over the same graph perform exactly the same counted work. Not approximately: identically. On a 2,070-vertex grid both examine 8,098 edge slots and both reach 2,070 vertices. On a random graph of 2,048 vertices and 6,144 edges both examine 12,288 slots and reach 2,048. Every counter this site has for measuring time reports them as indistinguishable, and it is right to.

They visit in wildly different orders, and the order does not cost anything.

Breadth-first and Depth-first, explicit stack on the same gridA 12 × 12 grid traversed from the top-left corner, shaded by the order in which each vertex was first reached — pale early, dark late. Both traversals perform 528 adjacency scans and 144 visits, exactly. What differs is how many vertices are held at once: 12 against 66. Every counter on this site except the space ones reports these as the same algorithm.Breadth-first — a ringfrontier peaks at 12 vertices heldDepth-first, explicit stack — a snakefrontier peaks at 66 vertices heldV = 144, E = 264identical in time, a factor apart in space
Fig. 2 A smaller grid traversed twice from the top-left corner, shaded by the order in which each cell was first reached. Breadth-first expands a ring; depth-first runs a snake down one column and back up the next. The two pictures could not look less alike and the two counts are identical — 528 adjacency scans and 144 visits each — while the frontiers peak at 12 vertices and 66.

What differs is how many vertices each has to hold at once. On that grid, breadth-first search’s queue peaks at 45 vertices and depth-first’s stack peaks at 1,012 — a factor of twenty-two, on the same graph, doing the same work.

That is a difference in space, and until this phase the site had no instrument for it either. It has one now, and the BFS-against-DFS comparison is the cleanest case for it in the whole collection: two algorithms that a complete accounting of time cannot separate, separated entirely by what they keep.

It is also worth noticing that the factor of twenty-two is a property of the graph, not of the algorithms. On a random sparse graph of the same size the two frontiers are 1,102 and 1,117 — indistinguishable. A random graph has a diameter of about logV\log V, so depth-first search cannot get deep, and it has an enormous middle layer, so breadth-first search cannot stay narrow. The grid has the opposite shape in both respects. The received advice that “BFS uses more memory than DFS” is true on a grid, false on a random graph, and stated about neither.

What the counts do not separate, and what that is good for

There is a temptation, having built an instrument, to use it on everything and report the differences. The more useful result here is a non-difference, and it took the instrument to establish it.

Three claims of the form “X is faster than Y” collapse under counting:

  • BFS against DFS: identical, as above.
  • Recursive DFS against the explicit-stack version: identical in every array count and in every graph count. The difference is that one of them uses the call stack and the other uses a heap-allocated one, which is a space and reliability question rather than a speed one.
  • Topological sort against a plain traversal: within a small constant. Kahn’s algorithm on the graphs here does about twice the adjacency scanning, because it counts in-degrees in a first pass and then decrements them in a second, and that is the entire cost of getting an ordering rather than a set.

A field whose first three findings are “these are the same” is a field with an instrument worth trusting. Instruments that only ever find differences are usually finding their own noise.

Which of the two terms is the large one

A graph bound is a sum of a term in VV and a term in EE, and the notation gives them equal billing. Measured, they are almost never comparable, and which of them dominates is decided by the graph rather than by the algorithm.

Breadth-first search, the same code, on the three families:

graph V E adjacency scans visits scans ÷ visits
grid 2,070 4,049 8,098 2,070 3.9
sparse, degree 6 2,048 6,144 12,288 2,048 6.0
dense, density 0.5 512 66,200 132,400 512 259

The scan count is always 2E2E — every undirected edge is walked from both ends — and the visit count is always VV. So the ratio is simply the average degree, and it runs from four to two hundred and fifty-nine over these three families.

Two consequences follow, and both are things a bound in V+EV + E obscures rather than states.

On a dense graph the VV term is noise. It is 0.4% of the work at density 0.5. Any optimisation of per-vertex bookkeeping — a cheaper visited-set, a smaller distance array, a better queue — is optimising a term that is four parts in a thousand. This is the same observation that takes Dijkstra’s queue apart, reached before any algorithm more complicated than a traversal is involved.

On a planar graph the two terms are within a factor of four. A grid has degree four by construction, and real planar-ish graphs — road networks, meshes, circuit layouts — are in the same range. For those, VV and EE are genuinely the same order and the sum in the bound is doing what the notation suggests.

So O(V+E)O(V + E) describes a family of situations spanning two orders of magnitude in which term matters, and the expression is identical across all of them. That is not a criticism of the bound, which is correct and tight; it is a statement about how much of the useful information a correct, tight bound can fail to carry.

The site’s response is procedural and small: every graph figure prints VV and EE in its caption strip, and the component figure exists so that the split can be seen rather than reasoned about.

Two representations, one graph

There is a second design decision inside the counted graph, and it turns out to carry an entire essay.

An adjacency structure can be held two ways. As lists: each vertex owns a list of edge records, allocated whenever the edge was created, scattered through memory in creation order. Or as CSR — compressed sparse row — a single flat array of every edge, sorted by source vertex, with an index saying where each vertex’s block begins.

Both are implemented here and both are counted identically, which is asserted rather than assumed: the gate requires the two to report the same number of edge scans, the same number of visits and the same trace length, and to differ in nothing except the addresses. If they ever diverged in counted work, any comparison between them would be comparing two different traversals.

What differs is only where the edges live, and that is enough to feed the cache model two very different traces from one traversal.

This is the two-counts theme at the widest gap it reaches anywhere on this site. No pair of sorting algorithms manages a factor of three and a half in modelled misses while agreeing exactly on the operation count, because sorting algorithms that agree on the operation count are usually doing nearly the same thing. Here the operations are not merely equal in number, they are the same operations in the same order, and only the addresses differ. The essay on the two layouts is about what follows from that.

The same sweep, four ways

One sweep on one regime is a picture of one regime, and the whole point of counting rather than quoting is that the counts can be taken again wherever the reader doubts them.

Counted work against V, dense, fixed densityEvery counted operation, summed, on logarithmic axes. The lines fan out because the algorithms differ in class rather than only in constant — at V = 64 the spread between best and worst is 122.4× and at V = 512 it is 1018×. On this sweep E is proportional to V², so E, V + E and V² coincide instead.10010⁴10⁵10⁶10⁷10⁸Vcounted workBreadth-firstDijkstra, binary heapDijkstra, all V queuedBellman–Ford, all passesV from 64 to 512, dense, fixed densitywork = scans + visits + relaxations + queue comparisons
Fig. 3 The same four algorithms on dense graphs at fixed density. Here EE is proportional to V2V^2, so EE, V+EV+E and V2V^2 coincide and the classes that were distinguishable on sparse graphs stop being so. The spread between best and worst is 122× at V = 64 and 1,018× at V = 512.

That is the first thing a sweep can settle and a table cannot: which distinctions the regime itself erases. Narrowing the set of algorithms settles the second.

Counted work against V, sparse, fixed average degreeEvery counted operation, summed, on logarithmic axes. The lines fan out because the algorithms differ in class rather than only in constant — at V = 64 the spread between best and worst is 2.3× and at V = 2048 it is 37×. On this sweep E is proportional to V, so V, E and V + E are the same line and the picture cannot tell them apart.10010³10³10⁴10⁵10⁶Vcounted workDijkstra, binary heapDijkstra, all V queuedV from 64 to 2048, sparse, fixed average degreework = scans + visits + relaxations + queue comparisons
Fig. 4 The two Dijkstras alone on sparse graphs, where the vertical scale is not being set by Bellman–Ford. The spread runs from 2.3× at V = 64 to 37× at V = 2,048 — the heap version’s advantage is a class difference and grows, which is invisible when the plate is scaled to hold a V3V^3 line.
Counted work against V, sparse, fixed average degreeEvery counted operation, summed, on logarithmic axes. The lines fan out because the algorithms differ in class rather than only in constant — at V = 64 the spread between best and worst is 108.1× and at V = 2048 it is 3509×. On this sweep E is proportional to V, so V, E and V + E are the same line and the picture cannot tell them apart.10010³10³10⁴10⁵10⁶10⁷Vcounted workBreadth-firstBellman–Ford, all passesV from 64 to 2048, sparse, fixed average degreework = scans + visits + relaxations + queue comparisons
Fig. 5 And the two ends of the same sparse sweep on their own: 108× at V = 64 and 3,509× at V = 2,048. Every number on all four of these plates is a count of operations the run actually performed, which is why the ratios can be quoted at all.

The last of the four is the one an implementer is actually choosing between, and it is worth putting on the dense regime because that is where a real graph usually sits.

Counted work against V, dense, fixed densityEvery counted operation, summed, on logarithmic axes. The lines fan out because the algorithms differ in class rather than only in constant — at V = 64 the spread between best and worst is 122.4× and at V = 512 it is 1018×. On this sweep E is proportional to V², so E, V + E and V² coincide instead.10010⁴10⁵10⁶10⁷10⁸Vcounted workBreadth-firstDijkstra, binary heapBellman–Ford, all passesV from 64 to 512, dense, fixed densitywork = scans + visits + relaxations + queue comparisons
Fig. 6 The same three on dense graphs, for the comparison that decides an implementation: 70.3× at V = 64 and 661× at V = 512. Dense or sparse changes which pairs are separable and does not change the ordering of any pair that is.

Where the graphs come from

Every graph here is generated from a stated seed, like every array on the site, and for the same reason: a figure built on Math.random() shows different numbers on every build and its caption is quoting numbers the reader’s copy does not contain.

Four families, and each exists because it answers a different question.

Sparse — a random graph at a fixed average degree, built by laying down a random spanning tree first and then adding edges until the target is met. Connected by construction, so every traversal measurement is over the whole graph. This is the family for sweeping VV with EE proportional to it.

Dense — every possible edge included with a stated probability. EE grows like V2V^2, and the fitted classes come out different on this family than on the last one, which is the whole reason both exist.

Grid — a square lattice. Planar, degree four, diameter V\sqrt{V}: the shape most real graphs of interest are nearer to than they are to a random one, and the family on which several published bounds stop describing what happens.

Path — the degenerate case, and the worst case for a depth-first stack.

Naming the family is not decoration. A graph bound with no stated density is a bound with a free variable in it, and the next essay is about how much damage that does.

Where the work goes, sparse, fixed average degree, V = 2048Each bar is one algorithm's counted work at V = 2048, split into the four primitives. The published bounds describe whichever segment the author had in mind, and which segment dominates is a property of the graph rather than of the algorithm: Dijkstra's queue comparisons are 64.1% of its work here.adjacency scansrelaxationsqueue comparisonsvisitsBreadth-first14,336Dijkstra, all V queued2,118,656Dijkstra, binary heap56,973Bellman–Ford, all passes50,309,120Prim64,884Kruskal74,008V = 2048, E = 6,144, sparse, fixed average degreeevery segment counted exactly
Fig. 7 The same total work, split into the four primitives, on sparse graphs. Which segment dominates is a property of the graph and of the queue rather than of the algorithm’s name — and the published bound for each of these algorithms describes whichever segment its author had in mind.

The primitive that is missing

Four counters cover what a traversal does, and there is a fifth operation that graph code performs constantly and that none of them charges for: asking whether a particular pair of vertices is joined.

It is absent here because no algorithm in this field performs it. Traversals and shortest-path algorithms enumerate a vertex’s neighbours; they never ask about a specific one. But a great many graph problems do — triangle counting, subgraph matching, clustering coefficients, anything that tests a candidate edge rather than following an existing one — and for those the operation is the dominant cost.

It is also the operation on which the two representations differ most starkly, and in the opposite direction from the sweep result. An adjacency matrix answers it in one read. An adjacency list answers it by scanning the vertex’s neighbours, which is the degree — so the same question costs one unit on a structure of Θ(V2)\Theta(V^2) bits and up to VV units on one of Θ(V+E)\Theta(V+E).

That is a genuine space-for-time trade with no dominant side, and it is the reason adjacency matrices survive at all on graphs small enough for V2V^2 to be affordable. Nothing in this field measures it, so nothing here says where the crossing is; the counter would be a fifth primitive and the algorithms that need it are a phase this collection has not written.

And the one that is counted is charged twice

One detail of the adjacency count is worth stating because every number in this field depends on it.

The graphs here are undirected, and an undirected edge appears in the adjacency structures of both its endpoints. So a traversal that examines every edge examines 2E2E slots, which is why the scan counts in the table above are exactly twice the edge counts.

On a directed graph the same traversal examines EE slots, because each edge is stored once. So a scan count is not comparable across directedness without dividing by the right factor, and a benchmark comparing an undirected traversal against a directed one is reporting a factor of two that is entirely about the representation.

The convention here is to count slots rather than edges, because a slot is what the algorithm actually touches and because the memory-layout results in this field are about slots. The caption strip prints VV and EE, and the factor between EE and the scan count is the thing a reader has to supply — which is stated here once so that it does not have to be repeated on every plate.

What this field is for

The foundation of this site measured one-dimensional arrays for time. A reader could reasonably have concluded that the method only works when the structure is that simple, and that conclusion would have been wrong in two separate ways.

Graphs are the first correction. The cost has two parameters instead of one, so the fit needs a second variable and a stated regime; the structures are pointers rather than indices, so the memory model has something much larger to say; and the field contains at least one bound — union–find’s — whose relationship between the formal class and the practical constant is the exact inverse of everything in the foundation.

Space is the second correction, and the two arrive together on purpose. An adjacency matrix costs Θ(V2)\Theta(V^2) memory and answers “is there an edge?” in constant time; an adjacency list costs Θ(V+E)\Theta(V + E) and answers it in time proportional to the degree. That is a space-against-time trade with no dominant answer, at a scale where it decides whether the problem fits in memory at all — which is the growth-factor argument again, several orders of magnitude further up.

The instrument in this essay is the smallest part of all that. It is four counters and a check that the traversal finished. What it buys is the right to say a number.

What this makes readable

Essays that name this one as a prerequisite.

Named alongside this one

Essays reaching for the same objects. Nobody chose these; they are what the concept index makes visible.

What links here

The 8 essays that link to this one and share the most of its objects, of 16 that link here.

The objects this essay names

Each one links to every other essay that touches it.

AdjacencyCost modelCounted primitiveDensityDijkstra's algorithmSearch frontierSparse graphTraceTraversal