What the machine does

A function with r pieces

Computed at every one of eight thousand positions across four texts, a function defined on the whole suffix array agrees exactly with r−1 anchors and one addition. Anchor it at the successor instead of the predecessor — one character of code — and it disagrees at 506 of 800 positions while still returning plausible numbers.

Here is a function on the positions of a text. φ(i)\varphi(i) is the position that comes immediately before ii when every suffix of the text is sorted — the suffix-array value of the row above ii’s own row.

It looks like a function that requires the suffix array. It does not. It requires r1r-1 pairs of numbers, where rr is the number of equal-letter runs in the Burrows-Wheeler transform, and on the collection drawn below that is 25 pairs for 129 positions.

phi over 129 positions, in 24 piecesEach point is a text position i against phi(i), the position that precedes it in suffix order. The function is a set of straight segments of slope one, and it breaks only where the row of i is the first row of a run in the transform — 23 times, against 26 runs. The circles mark the anchors the structure stores; between two of them phi is one addition, so r values answer the function at all 129 positions.text position iphi(i)anchor: a run start25 of thema text that repeats itself · 4 copies of 32r = 26 · pieces 24
Fig. 1 The whole function over a short collection. Straight segments of slope one, broken at the circles, which are the only positions the structure stores anything for.

The one line of algebra

The transform’s LF mapping sends a row to the row whose suffix begins one character earlier. Inside a run of equal characters it is order-preserving by construction: if rows x1x-1 and xx carry the same character then LF(x)=LF(x1)+1\mathrm{LF}(x) = \mathrm{LF}(x-1) + 1, because the rank of that character at xx is one more than at x1x-1 and the two land in the same block of the first column.

Now take suffix-array values through that. Write i=SA[x]i = \mathrm{SA}[x], so that φ(i)=SA[x1]\varphi(i) = \mathrm{SA}[x-1] by definition. The row of i1i-1 is LF(x)\mathrm{LF}(x) and the row of φ(i)1\varphi(i)-1 is LF(x1)\mathrm{LF}(x-1), and if those two rows are adjacent then the positions they name are adjacent in φ\varphi as well:

φ(i1)=φ(i)1unless the row of i begins a run.\varphi(i-1) = \varphi(i) - 1 \qquad \text{unless the row of } i \text{ begins a run.}

That is the whole derivation, and everything else in this essay is a consequence of it. Between two consecutive positions whose rows begin runs, φ\varphi is a straight line of slope one. There are rr run beginnings, so there are at most rr segments, so the function is determined by rr numbers.

The pieces phi has, against the runs it is allowedFour texts of 2,000 characters. The bar is the number of positions where phi is not its predecessor plus one — the pieces the function genuinely has — and the number beside it is r, which is the bound. The bound is never exceeded and is never quite reached: an anchor whose segment happens to continue is stored and not needed, which costs bits and cannot be detected without computing the function.a text that repeats itself124r = 126English-like640r = 642four symbols, uniform1,515r = 1,517periodic, period 171,735r = 1,737n = 2,000 in every rowpieces of phi · r beside each2,000 characters
Fig. 2 The pieces the function genuinely has on four texts of two thousand characters, against the run count that bounds them. Never exceeded, and short by one every time.

Short by exactly one, every time, and the reason is not rounding

The plate above measures the pieces rather than assuming them: walk every position, compare φ(i)\varphi(i) against φ(i1)+1\varphi(i-1)+1, and count the disagreements. On a repetitive text of two thousand characters there are 124 of them for 126 runs; on English 640 for 642; on a four-symbol text 1,515 for 1,517.

Two short, every time, and both are accounted for.

The first run’s beginning is row 0, which has no row above it, so it anchors nothing: there are r1r-1 anchors rather than rr. The second is subtler and it is worth stating because it looks like an off-by-one and is not. One anchor is always at text position 0, and position 0 has no predecessor to compare against, so it cannot register as a jump. The anchor is real, the structure stores it, and the audit that counts jumps cannot see it.

So the identity is jumps=r2\text{jumps} = r - 2 and the anchors are r1r-1, and a check demanding equality would fail on every text. This collection’s standing rule about assertions applies exactly here: the count somebody chose is about picking the quantity deliberately, and the quantity worth asserting is that the jumps are a subset of the anchors, which is the claim the structure rests on.

What an anchor stores, which is less than it looks

An anchor is a pair: a text position where the function jumps, and the value it takes there. The positions are a sparse bit vector over the text and the values are — nothing new.

φ\varphi at an anchor SA[p]\mathrm{SA}[p], where pp begins a run, is SA[p1]\mathrm{SA}[p-1]. And p1p-1 is the last row of the previous run, whose suffix-array value the structure already keeps for a different purpose. So the anchor carries a run index rather than a position, at log2r\lceil\log_2 r\rceil bits, and the value is read out of an array that exists anyway.

That is why the sampling in the sampling that follows the runs is 2r2r values and not 3r3r. Two arrays, three uses, and the essay’s headline size would be half as large again if the sharing were not exact.

phi over 145 positions, in 37 piecesEach point is a text position i against phi(i), the position that precedes it in suffix order. The function is a set of straight segments of slope one, and it breaks only where the row of i is the first row of a run in the transform — 36 times, against 38 runs. The circles mark the anchors the structure stores; between two of them phi is one addition, so r values answer the function at all 145 positions.text position iphi(i)anchor: a run start37 of themEnglish-like · 3 copies of 48r = 38 · pieces 37
Fig. 3 The same function on a collection of three drifted copies rather than four identical ones. More pieces, same shape, and the segments are still exactly slope one.

What it costs to evaluate

A φ\varphi is a predecessor query over rr anchors and one addition. The predecessor is a binary search here — log2r\lceil\log_2 r\rceil comparisons, charged as a rank in the index counter, because that is what the field charges a directory lookup as.

It is worth being clear about what this is not. It is not an LF step. An LF step walks the transform: it accesses a character, ranks it, and moves one position back in the text. A φ\varphi step moves one row up the suffix array, which is a different direction through a different structure, and the two happen to be inverse-ish in a way that is easy to misremember and would produce a working-looking search that returns the wrong positions.

The plate that compares them says which operation it is counting for that reason. Rank is the only thing it does is where this collection established that a rank, a select and a character comparison are three acts rather than three names, and the same discipline is what keeps this comparison honest.

What one occurrence costs, by sampling policy80 occurrences of a 6-character pattern in a collection of 8,193 characters with r = 95. The r-index spends exactly one predecessor query per occurrence after the first and takes no LF step at all; a regular sampling walks back to the nearest sampled row, which costs up to its own rate. The bars are in different operations and are drawn on one axis only because the question is how many of anything.r-index: phi per occurrence1.0predecessorregular, one in 83.0LF stepsregular, one in 167.8LF stepsregular, one in 3214.2LF stepsregular, one in 6433.4LF stepsa text that repeats itself · 16 copies · pattern " the t"operations per occurrence located80 occurrences
Fig. 4 One predecessor query per occurrence against up to thirty-two LF steps, with the units named. The r-index takes no LF step during a locate at all, which the plate asserts rather than states.

Applying it repeatedly is the locate

The rows of a search interval are consecutive, and φ(SA[x])=SA[x1]\varphi(\mathrm{SA}[x]) = \mathrm{SA}[x-1] by definition. So from one known occurrence — the suffix-array value at the interval’s last row — every other occurrence is one φ\varphi away from the one before it.

An interval of width 80 costs 79 applications and no walks. Measured on a collection of sixteen copies, a six-character pattern with 112 occurrences costs 111 φ\varphi steps, zero LF steps, and 144 ranks in total, against a regularly sampled index’s 1,136 LF steps for the same eighty occurrences at one value in thirty-two.

The known occurrence has to come from somewhere, and it comes from the search rather than from a walk: the occurrence carried through the search is that half. Without it, φ\varphi has nothing to start from and the structure has to walk to a boundary — which is every occurrence at the same price, where the walk is measured and is thousands of steps long.

The trade a sampling rate buys, and the point that is off itEach dot is a regular sampling rate on the same collection of 8,193 characters: its bits against the LF steps one occurrence costs. The rate is the only dial, and it moves the two together. The square is the r-index, at 10,244 bits and 0.99 predecessor queries an occurrence — a different act from an LF step, which is why the axis says steps and the caption says which. The nearest rate that matches it for steps is one in 2, at 69,649 bits, 6.8 times as many. Both axes are logarithmic.10,000110sampling, bitssteps per occurrence1 in 21 in 41 in 81 in 161 in 321 in 641 in 128r-indexEnglish-like · 16 copies · r = 233pattern " time "
Fig. 5 What the function buys, in the only comparison that is like for like: bits against steps. A regular sampling reaching one step per occurrence costs nearly seven times the bits.

The exactness check, and why it is the whole function

An identity like the one above is exactly the sort of claim that is true, is believed, and is implemented backwards. So it is checked the way this collection checks a claim it depends on: at every position of four texts, against the real suffix array, with no sampling and no tolerance.

Eight thousand positions, four texts, zero disagreements. That is not a spot check and it is not a property test with a seed — it is the function, compared against the definition, everywhere it is defined.

The check is cheap because the texts are small, and it is worth its cost because the failure mode is silent. A φ\varphi built wrongly still returns numbers in range, still returns positions in the text, and still produces the right answer for patterns whose occurrences happen to lie inside one segment. A search over a repetitive collection has many such patterns.

The pieces phi has, against the runs it is allowedFour texts of 1,500 characters. The bar is the number of positions where phi is not its predecessor plus one — the pieces the function genuinely has — and the number beside it is r, which is the bound. The bound is never exceeded and is never quite reached: an anchor whose segment happens to continue is stored and not needed, which costs bits and cannot be detected without computing the function.a text that repeats itself112r = 114English-like499r = 501four symbols, uniform1,143r = 1,145periodic, period 171,303r = 1,305n = 1,500 in every rowpieces of phi · r beside each1,500 characters
Fig. 6 The same audit at a different length. The relation between pieces and runs is a property of the transform rather than of the text’s size, which is what makes it usable as a size claim.

Two functions that both move by one, and are not each other

The commonest way to implement this wrongly is not a wrong anchor. It is to reach for the function the structure already has.

An index of this kind carries LF, and LF is a step of one: one character earlier in the text. φ\varphi is also a step of one: one row earlier in the suffix array. Both are cheap, both are already implemented, both return something plausible, and they are not inverses of one another in any sense that a locate can use.

The distinction is worth drawing out because it is the same confusion the two costs of an index rest on. Walking LF from a row reaches a sampled row, and what it produces is the current row’s own position once the number of steps taken is added. Applying φ\varphi to a position reaches another occurrence, and it produces that occurrence directly with nothing to add. The first is a walk whose length is set by a rate somebody chose; the second is a single query whose cost is set by logr\log r.

That is why the two are drawn on plates that name their operation, and why the total for a locate is reported as phi steps and LF steps separately in every measurement here. Summing them would give a number belonging to neither policy, which is the failure one run, two counts is about and the count is not the time is the older statement of.

There is one more reason to keep them apart. LF’s cost depends on the transform’s representation — on a run-length structure it is a rank on the run heads and a select on the lengths, which is three counted operations rather than one — while φ\varphi’s cost does not depend on the transform at all. A change to how the transform is stored moves one of these numbers and not the other.

Searching 2,048 characters for "s than": 4 occurrences, 0 comparisonsEach row is the suffix-array interval after one more character of the pattern has been consumed, right to left. The bar spans the rows of the sorted suffixes that begin with the part of the pattern read so far, and it is computed by two rank queries on the transform: no character of the text and no character of the pattern is ever compared with anything. When the pattern runs out the width of the interval is the number of occurrences, and it is known before any occurrence has been located.all rows2,049...n103 rows2 ranks...an42 rows4 ranks...han14 rows6 ranks...than14 rows8 ranks... than14 rows10 ranks...s than4 rows12 ranksEnglish-like, 2,048 characters · one row per character consumed0 character comparisons
Fig. 7 The search the interval comes from, which performs no character comparison and produces no position. Everything in this essay happens after this picture ends.

The check that must fail

The exactness check is only worth what its counterexample is worth, so there is one, and it is one character of code.

The segments are anchored at the predecessor: φ(i)=φ(s)+(is)\varphi(i) = \varphi(s) + (i - s) where ss is the largest anchor at or below ii. Anchor them at the successor instead — the smallest anchor at or above ii, which is the same array read from the other end — and the function is wrong at 506 of 800 positions on an ordinary English text.

Not all of them. That is the point of quoting the number: a third of the positions still come out right, because a position whose block happens to be short lands in the same segment either way. A test on a handful of positions has a real chance of passing, and the assertion that catches it is the one that walks the whole function.

The direction of the algebra is the entire content of the structure, and it is a single comparison operator in a binary search. This collection has recorded that shape before — measured, not asserted is the standing rule and a search that runs backwards is where it first mattered in this field.

The sampling that stops growing, and the one that does notEnglish-like of 512 characters repeated up to 32 times, identically. The upper curve is the run-length index's regular sampling at one value in 32; the lower is the r-index's two values per run. Across the sweep the first grows 39.05x and the second 1.49x, and the second's growth is entirely the ⌈log₂ n⌉ each value is written in — r itself does not move once there is a second copy. Both axes are logarithmic.1,00010,00010³10⁴bits12481632characters in the collection · copies aboveregular, one in 32run boundaries, 2rEnglish-like · divergence 0r = 233 at n = 16,385
Fig. 8 The size claim the function makes possible, with the sampling it replaces above it. Neither line means anything until the function is known to be exact, which is why the audit comes first.

A second check, from the other end of the interval

There is a second way to get this wrong that the exactness check cannot see, and it needs its own counterexample.

The occurrences of a pattern are the suffix-array values of the rows in its interval. Enumerating them with φ\varphi requires starting at the interval’s last row and walking up, because φ\varphi goes up. Start at the first row instead — also an occurrence, also correct, also easy to reach — and the walk leaves the interval immediately and returns positions belonging to other patterns entirely.

Measured: a five-character pattern with twelve occurrences returns twelve positions either way, and the wrong twelve are not the right twelve. The count is right, the type is right, every value is a position in the text, and the answer is wrong. Only a comparison against exhaustive search catches it, which is why the gate compares against exhaustive search rather than against the other index.

The same samples, with and without phi80 occurrences of a 6-character pattern in a collection of 8,193 characters with r = 95. The r-index spends exactly one predecessor query per occurrence after the first and takes no LF step at all; a regular sampling walks back to the nearest sampled row, which costs up to its own rate. The last row is the same run-boundary samples with no phi to enumerate from: the walk is bounded by nothing and reaches 6,907 steps for one occurrence.r-index: phi per occurrence1.0predecessorregular, one in 83.0LF stepsregular, one in 167.8LF stepsregular, one in 3214.2LF stepsregular, one in 6433.4LF stepsrun boundaries, walked2954.9LF, unboundeda text that repeats itself · 16 copies · pattern " the t"operations per occurrence located80 occurrences
Fig. 9 The alternative to having the function at all: the same run-boundary samples with no phi, walked. Two thousand nine hundred LF steps an occurrence, against one predecessor query.

Where the pieces come from, and where they do not

One more property is worth stating because it is what makes this a claim about repetition rather than about suffix arrays in general.

The pieces are the runs, and the runs are what a repetitive collection has few of. On a text of two thousand characters over four symbols the transform has 1,517 runs and the function has 1,515 pieces — nearly one per position, and the structure that stores them is storing the suffix array by another name. On thirty-two copies of one text the runs stay at 233 whatever the length, and the function over sixteen thousand positions is 232 anchors.

So φ\varphi is not a compression trick that works on any text. It is the same measurement the rest of this strand is about, applied to a function rather than to a structure: the number of pieces is rr, and rr is what a copy does not increase.

What this makes possible that a rate cannot

It is worth saying plainly what the function buys, because “the same answer with fewer bits” undersells it and “constant-time locate” overstates it.

A regular sampling has one dial and it moves two costs together. Halve the rate and the sampling doubles and the walk halves; there is no setting at which both are small, and the product is fixed by the structure rather than by the text. Every point on that curve is available and none of them is off it. The frontier between time and space is this collection’s name for that shape, met here in a structure rather than in a sort.

The function moves the whole curve. Its cost per occurrence is a predecessor query whatever the text, and its size is rr anchors — so the trade is not between the two, it is between the size and a property of the text. A collection that repeats has few anchors and the structure is small and fast at once; a text that does not repeat has many, and the structure is large and still fast.

That is a different kind of statement from a rate, and it is the reason the r-index is described by its own literature as achieving something a sampling cannot rather than as a better-tuned sampling. What this essay adds is the measurement: on a collection of sixteen copies the sampling is 10,244 bits at one predecessor query an occurrence, and the nearest regular rate reaching that step count is one value in two, at 69,649 bits.

The piece count is the compression factor, written out

“The number of pieces is rr” is the claim, and turning it into a size takes one ratio — which is worth doing, because the ratio spans a factor of ninety across the four texts this essay already measures.

The suffix array the function replaces is nn entries of log2n\lceil\log_2 n\rceil bits. The anchors are rr entries of log2r\lceil\log_2 r\rceil bits, since an anchor carries a run index rather than a position. So the function’s payload against the array it stands in for is

rnlog2rlog2n.\frac{r}{n}\cdot\frac{\lceil\log_2 r\rceil}{\lceil\log_2 n\rceil}.

Evaluate it on the audit’s own texts, all at two thousand characters. The four-symbol text has 1,517 runs: (1517/2000)×(11/11)=0.76(1517/2000)\times(11/11) = \mathbf{0.76}. English has 642: (642/2000)×(10/11)=0.29(642/2000)\times(10/11) = \mathbf{0.29}. The repetitive text has 126: (126/2000)×(7/11)=0.04(126/2000)\times(7/11) = \mathbf{0.04}. And thirty-two copies of one text hold 233 runs over sixteen thousand positions: 0.008\mathbf{0.008}.

Ninety-fold, from one quantity, on four texts that are all the same length.

That makes the essay’s closing warning precise rather than rhetorical. On a four-symbol text the function is not “storing the suffix array by another name” — it is storing 76% of it, which is a genuine 24% saving and is nowhere near enough to justify the machinery around it. The predecessor structure, the run heads and the toehold’s arrays are all themselves proportional to rr, so at r/nr/n near one every term in the structure is proportional to nn and nothing asymptotic is left: what remains is a compressed index that is a constant factor smaller than an uncompressed one, competing against structures designed for that regime.

At the other end the ratio is what the field is for. Eight thousandths means an object a hundred and twenty times smaller than the array it replaces, with the same locate cost per occurrence, and the whole of that comes from rr not growing when a copy is added.

Two things follow that are worth carrying.

The break-even is a property of the text and is computable in one pass. The run count is a scan of the transform; nn is the length; the ratio decides whether the structure is worth building before any of it is built. That is the same instruction a sampling that costs more than the array arrives at from the sampling’s side, where the same quantity decides whether 2r2r values are smaller than the array they index.

And the second factor is not negligible. The ratio of logarithms is log2r/log2n\lceil\log_2 r\rceil/\lceil\log_2 n\rceil, which is 0.64 on the repetitive text and 0.57 at thirty-two copies — so a third of the saving on a highly repetitive collection comes from the anchors being narrower, not from there being fewer of them. That term disappears from any account written as O(r)O(r), and it is worth a third of the structure. The sampling that follows the runs charges it explicitly for the same reason.

The honest limit

The predecessor query here is a binary search over an array of anchors. A published r-index uses a structure with a better bound, and the difference matters for the constant rather than for the claim — but it means the operation counts in this essay are a binary search’s, and a plate that compared them against an implementation using a faster predecessor would be comparing two things.

The other limit is that the segments are drawn and measured at sizes where the whole function fits on a page. The identity is exact at every size and the picture is only legible at a hundred positions, so the plate that shows the shape and the sweep that establishes it are different measurements on different texts, which is stated on both rather than blurred.

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

Every essay whose body links to this one.

The objects this essay names

Each one links to every other essay that touches it.

Backward searchBurrows-wheeler transformCounterexampleFalsificationFM-indexLocateMeasurementPhi functionPredecessor queryR-indexRun boundary samplingRun-lengthSelf-indexSuffix array