The other axis

Two bits a value, and what undoes them

The parentheses of a range minimum over 65,536 values are 131,072 bits. Everything that makes them answerable is 651,800 more — five times the payload — and one encoding choice nobody quotes accounts for a sixth of it on its own.

A structure quoted at “2n + o(n) bits” is quoted at its payload. The second term is the subject of this essay, and on every size drawn here it is several times the first.

The shape a range question is about got as far as a sequence of 2n parentheses and a rule for reading an answer out of it. The rule needs two things the sequence does not provide: a way to get from an array index to its own parenthesis, and a way to find the lowest excess between two points without walking the whole range. Both are structures, both cost bits, and one of them is where the encoding choice from the previous essay stops being a curiosity.

2n bits, and everything that makes them answerableA range minimum over 65,536 values. The parentheses are 131,072 bits — the 2n the structure is quoted at — and everything below them is what turns a sequence of bits into a structure that answers a query: the support that finds the k-th opening, and the two layers of block minima. Together they come to 651,800 bits, which is 4.97x the payload. The segment tree this replaces is 2,228,224 bits, so the whole structure is 2.85x smaller — a real saving, and not the hundredfold one "2n + o(n)" invites. The in-block lookup table is a further 82,944 bits and is not counted here, because it belongs to the block length rather than to this structure.parentheses131,07216.7%select support115,71214.8%superblock minima16,3982.1%superblock table80,97010.3%block minima147,46518.8%block tables291,25537.2%the partsbitstotal 782,872 · the segment tree 2,228,224shared lookup table 82,944, not counted65,536 values · openings in index order, found by select2.85x smaller
Fig. 1 Every part of the structure over 65,536 values. The top bar is the 2n the structure is quoted at; the rest is what makes it answer.

The bill

Over sixty-five thousand values the parentheses are 131,072 bits. The rest comes to 651,800, so the whole structure is 782,872 — five times its own payload.

That is not a scandal and it is not an implementation failure. It is what o(n) looks like at a size anybody runs. A term that grows more slowly than n is eventually smaller than n; sixty-five thousand is nowhere near eventually. An index larger than what it indexes made the same observation about a self-index and it holds here with a different constant: the asymptotic statement is true, the structure is smaller than what it replaces, and the ratio it is smaller by is not the ratio the notation suggests.

2n bits, and everything that makes them answerableA range minimum over 4,096 values. The parentheses are 8,192 bits — the 2n the structure is quoted at — and everything below them is what turns a sequence of bits into a structure that answers a query: the support that finds the k-th opening, and the two layers of block minima. Together they come to 40,782 bits, which is 4.98x the payload. The segment tree this replaces is 106,496 bits, so the whole structure is 2.17x smaller — a real saving, and not the hundredfold one "2n + o(n)" invites. The in-block lookup table is a further 9,408 bits and is not counted here, because it belongs to the block length rather than to this structure.parentheses8,19216.7%select support6,97614.2%superblock minima1,3722.8%superblock table3,9628.1%block minima10,92822.3%block tables17,54435.8%the partsbitstotal 48,974 · the segment tree 106,496shared lookup table 9,408, not counted4,096 values · openings in index order, found by select2.17x smaller
Fig. 2 The same accounting at a sixteenth of the size. The overhead’s share falls as the array grows, which is what o(n) means when it is drawn rather than asserted — and it falls slowly.

The comparison that matters is with the segment tree the document listing was measured against, which is 2n values of ⌈log₂ n⌉ bits — 2,228,224 at this size. The succinct structure is 2.85 times smaller, which is a real saving worth building for. It is not the seventeenfold saving 2n against 2n⌈log₂ n⌉ invites.

The two structures, as the array growsThe segment tree is 2n values of ⌈log₂ n⌉ bits and follows the array's size times its logarithm. The succinct structure's payload is exactly two bits a value; its total is that plus an overhead which grows more slowly than the payload does but is several times larger than it at every size drawn. At 65,536 values the two totals are 2,228,224 and 782,872, a factor of 2.85x — and they answered all 40 random ranges identically at every size. Both axes are logarithmic.10,00010⁴10⁵10⁶valuesbitsthe segment treesuccinct, totalits payload4 sizes · 40 queries each2.85x apart
Fig. 3 The two totals as the array grows, with the payload drawn separately. The gap between the middle and lower lines is everything this essay is about.

It is worth putting that ratio beside the one the strand it serves already has. An index larger than what it indexes measured a plain suffix array at nearly four times its own text, and a compressed index at a fraction of it; the interesting number in both cases was never the leading term. The same is true here, and this strand differs only in that the leading term is small enough to be quoted as the whole thing.

The map, which is where the encoding choice lands

The rule needs, given index i, the position of i’s opening parenthesis. There are two ways to have it.

Write it down. An array of n entries, each a position in a sequence of 2n, is n⌈log₂ 2n⌉ bits. At this size that is 1,114,112 — eight and a half times the payload, and half the segment tree, spent on the one part of the structure nobody quotes.

Compute it. If the openings are in index order — which is exactly what the stack construction gives and what an Euler tour does not — then index i’s opening is the (i+1)-th one, and finding the k-th one is a select. Support for that over the same sequence is 115,712 bits.

The map from an index to its place in the sequenceAt 65,536 values the parentheses are 131,072 bits. Writing down where each index's opening is costs 1,114,112 — 8.50x the payload, and half the segment tree, spent on the one part of the structure nobody quotes. Written in index order the map is a select instead, and select support over the same sequence is 115,712 bits, which is 9.63x smaller. Neither answers a query on its own; both are the price of asking one.the parentheses131,0721.0x payloadselect support115,7120.9x payloada table of positions1,114,1128.5x payloadthe segment tree2,228,22417.0x payloadbits65,536 values9.63x apart
Fig. 4 The map, both ways, beside the payload and the tree. The bar that is nearly the segment tree is a lookup table nobody counts as part of the structure.

A factor of 9.6, from a decision about what order to write the parentheses in. Both encodings are correct. Both give a balanced sequence of exactly 2n bits. One of them makes the structure 782,872 bits and the other makes it 1,781,272, which is only 1.25 times smaller than the segment tree it was supposed to replace.

This is why the previous essay spent a section on which order the pushes happen in. The whole of the difference between “a succinct range minimum” and “a segment tree with extra steps” is that one line.

The map from an index to its place in the sequenceAt 32,768 values the parentheses are 65,536 bits. Writing down where each index's opening is costs 524,288 — 8.00x the payload, and half the segment tree, spent on the one part of the structure nobody quotes. Written in index order the map is a select instead, and select support over the same sequence is 57,344 bits, which is 9.14x smaller. Neither answers a query on its own; both are the price of asking one.the parentheses65,5361.0x payloadselect support57,3440.9x payloada table of positions524,2888.0x payloadthe segment tree1,048,57616.0x payloadbits32,768 values9.14x apart
Fig. 5 The same comparison over three smaller arrays. The ratio between the two maps is fixed by the encoding rather than by the size, so it does not close.

What select support costs, and why it is not free either

Support for finding the k-th one in a bit vector is a structure in its own right, and select is not rank backwards is the essay that prices it. The short version is that rank’s directory is indexed by position and select’s answers are indexed by rank, and the ones are not evenly spaced — so a fixed block length bounds one and bounds nothing about the other.

Here the vector is half ones by construction: a balanced parenthesis sequence has exactly n of each. That is the easy case for select support, and it is worth saying so, because the same support over a sparse vector costs more per one rather than less. The 115,712 bits is a structure operating in its best regime.

There is a second reason the parenthesis sequence is a kind vector to support: its ones are spread evenly. A run of a thousand closings cannot happen — the excess would go negative — so the gaps between ones are bounded by the tree’s height, and the sparse case that costs select support most of its bits never arises. That is a property of what the vector is, not of the data it came from, and it is the sort of thing worth checking rather than assuming, because a vector with the same density and clustered ones costs measurably more.

The other term: two layers of blocking

The rule’s second need is the lowest excess between two positions, and answering it without walking is a second structure.

The excess is cut into blocks of about half a logarithm. A block’s minimum is stored; a table over the block minima answers any span of whole blocks. That is the classical construction, and the table that fits inside a block is about the part inside a block. This essay is about the part above it, because that is where a second surprise lives.

One layer of blocking is not o(n). A table over every block, answering any span of them in constant time, has (N/b)·log₂(N/b) entries of ⌈log₂ N⌉ bits. At the block length that keeps the in-block table small, that comes to 3,605,987 bits at this size — larger than the segment tree the whole structure was replacing, by a factor of 1.6.

One layer of blocking is not o(n)The overhead of each version against the payload it sits on. With one layer the table over every block has (N/b)·log(N/b) entries of ⌈log₂ N⌉ bits, which at 65,536 values is 3,605,987 bits — 28x the payload and rising faster than it. With two layers it is 651,800, which is 4.97x the payload and rising more slowly. Both answer in a constant number of lookups; the second takes about twice as many. Both axes are logarithmic.10,00010⁴10⁵10⁶valuesbitsone layertwo layersthe parentheses4 sizes · block 85.53x apart
Fig. 6 One layer of blocking against two, as the array grows. The upper line is the version most accounts of this structure describe.

That is not a subtle point and it is routinely elided. The structure described as “linear space” is linear in words, and a word is a logarithm of bits. On an array of machine integers that distinction is invisible, because the input is words too. On a structure whose whole claim is that it holds two bits a value, it is the difference between a claim and its opposite.

The second layer

The fix is a second level of grouping. Blocks are gathered into superblocks of about a logarithm of them; a table over the superblock minima answers spans of whole superblocks, and a table over the blocks inside one superblock answers the rest — with its entries being offsets within a superblock rather than positions in the whole sequence, so each is a logarithm of a logarithm rather than a logarithm of n.

Every term is then o(N), and the measured overhead falls from 3,605,987 to 651,800: a factor of 5.5, for a query that touches about twice as many tables.

Both versions are exact, both are constant-time, and the choice between them is bits against lookups with nothing else in it. That is the cleanest trade in this strand and it is the one nobody is offered, because the one-layer version is what gets described and the two-layer version is what gets shipped.

It is also the reason this collection reports both. A structure that is smaller and slower and a structure that is larger and faster are two answers to a question the reader is asking, and printing only the one that flatters the strand’s argument would be the constant the notation drops committed on purpose.

The table that belongs to nobody

One term is reported separately and deliberately not summed into the total: the in-block lookup table.

It is indexed by a block’s bit pattern, so it depends on the block length and on nothing else. Two range minima with the same block length share it; a collection holding a hundred of them pays for it once. At the block length used here it is nine thousand four hundred and eight bits — a real cost, and one that is charged to the wrong structure if it is charged to any single one of them.

Reporting it inside the total would make a lone structure look worse than it is and a hundred structures look nine hundred times worse than they are. Reporting it not at all would let the phrase “constant time” be bought with an exponential table nobody mentions, which is the exact shape of the error the operations a candidate count leaves out records for a different structure. So it is printed beside every total and summed into none of them.

What this changes about the thing it was built for

The document listing holds five parts: the suffix array, the text, the document array, the chain of previous occurrences, and the range minimum over the chain. The first two answer where; the last three answer which.

With a segment tree those last three come to 2.40 times the first two. That number is why the essay that measured it called the listing apparatus “the largest single structure in the strand and the one least like its published form”.

With the succinct structure they come to 1.47 times. The range minimum itself falls from 62% of the listing apparatus to about a third of it, and the chain and the document array — neither of which this strand touches — become the larger half.

That is the difference between a structure a reader would think twice about bolting on and one they would simply build. It is also the point at which the interesting question moves elsewhere: the chain is n⌈log₂ n⌉ bits and nothing here makes it smaller.

What a size like this is checked against

Every number above is computed from a structure’s shape rather than from what a language allocates, which is this collection’s rule everywhere and matters more here than usual: a bit vector held as one byte per bit would report eight times these figures and answer every query identically.

Three checks stand behind the numbers.

The parts must sum to the total, and the total must be what is compared. A size that omits a directory is the shape of every over-claimed compression figure in this collection, and the structure paid for before the first query records what it did to a different structure’s headline.

Both encodings must answer identically. The select-mapped and array-mapped structures are run against the segment tree on four hundred random ranges and required to return the same position — not merely the same value, since a tie makes those different claims.

And the shared table must be shared. Two structures of the same block length are built and required to report the same shared figure and to be holding the same table object. A per-structure table would be correct and would make the o(n) claim false, silently, at exactly the sizes where the exponential in the block length starts to bite.

2n bits, and everything that makes them answerableA range minimum over 16,384 values. The parentheses are 32,768 bits — the 2n the structure is quoted at — and everything below them is what turns a sequence of bits into a structure that answers a query: the support that finds the k-th opening, and the two layers of block minima. Together they come to 1,005,616 bits, which is 31x the payload. The segment tree this replaces is 491,520 bits, so the whole structure is 0.47x smaller — a real saving, and not the hundredfold one "2n + o(n)" invites. The in-block lookup table is a further 32,768 bits and is not counted here, because it belongs to the block length rather than to this structure.parentheses32,7683.2%first-occurrence array245,76023.7%block minima74,9127.2%sparse table684,94466.0%the partsbitstotal 1,038,384 · the segment tree 491,520shared lookup table 32,768, not counted16,384 values · an Euler tour, with the positions written down0.47x smaller
Fig. 7 The same accounting for the version with one layer of blocking and a written-down map — every part correct, every part measured, and a total larger than the structure it replaces.

Where the remaining bits actually are

It is worth breaking the 651,800 down, because the shape of it is not what the words “block minima” suggest.

The superblock minima and the table over them are small — a few tens of thousands of bits between them, because there are only about a thousand superblocks. The block minima are 147,000: one entry per block, and there are sixteen thousand blocks, each entry an offset inside its superblock rather than a position in the whole sequence.

The largest single term is the tables inside the superblocks — a table over each superblock’s blocks, at 409,000 bits, which is 63% of all the overhead. Each of its entries is small, and there are a great many of them: one per block per level of the within-superblock table.

That is the term a further layer would attack, and the reason there is not one here is that the returns fall off sharply. A third layer would replace log log N entries per block with log log log N, which at any size that exists is the difference between five and three. The published structures instead attack it from a different direction — by giving up the block minima entirely and answering within a superblock from the parenthesis sequence itself — and that is a different structure rather than a further layer.

2n bits, and everything that makes them answerableA range minimum over 32,768 values. The parentheses are 65,536 bits — the 2n the structure is quoted at — and everything below them is what turns a sequence of bits into a structure that answers a query: the support that finds the k-th opening, and the two layers of block minima. Together they come to 315,964 bits, which is 4.82x the payload. The segment tree this replaces is 1,048,576 bits, so the whole structure is 2.75x smaller — a real saving, and not the hundredfold one "2n + o(n)" invites. The in-block lookup table is a further 82,944 bits and is not counted here, because it belongs to the block length rather than to this structure.parentheses65,53617.2%select support57,34415.0%superblock minima8,1942.1%superblock table34,5249.0%block minima73,73719.3%block tables142,16537.3%the partsbitstotal 381,500 · the segment tree 1,048,576shared lookup table 82,944, not counted32,768 values · openings in index order, found by select2.75x smaller
Fig. 8 The parts at a different size and a different number of documents. The ordering of the terms is stable; the within-superblock tables are the largest one at every size drawn in this strand.

The other thing that breakdown makes visible is how little of the overhead has anything to do with the range minimum. The select support is a sixth of it, and select support is the price of the encoding rather than of the query. Take the map away — pretend for a moment that an index arrives with its parenthesis position already known — and the structure is 667,160 bits, of which 131,072 is payload. The question this structure answers costs about four times its own payload to be able to answer; the question of where to start asking costs the rest.

How the last sixty-three per cent is removed

The breakdown ends by naming the within-superblock tables as 63% of all the overhead and saying that the published structures attack it from a different direction. That direction is worth setting out, because it is the same move this collection has already measured in a completely different subject.

The tables exist so that a span of blocks inside one superblock can be answered without looking at the parentheses. Remove them and the query has to find the lowest excess in that span by reading the parenthesis sequence directly — which sounds like giving up the constant-time guarantee and is not, provided the span is short enough.

Choose the superblock so that it spans a number of parentheses that fits in a machine word. Then the whole span is one load, and the lowest excess within it is computed by a fixed sequence of word operations — the running excess of a bit pattern is a prefix sum of ±1, and a prefix sum over a word’s worth of bits is a handful of shifts, masks and a population count. A bounded number of word operations is a constant, and it is a constant with no table behind it.

That is the column computed in machine words arriving in a structure that has nothing to do with strings: a per-element loop replaced by a fixed number of operations on a register, with the register’s width setting how much can be replaced. The two cases even fail the same way — the technique works while the object fits in a word and steps to a second word when it does not.

Three consequences are worth having.

The bits genuinely go. Sixty-three per cent of the overhead is a table that a word operation replaces, so the structure drops from five times its payload to under two — which is the difference between an accounting curiosity and a structure that is nearly what it is quoted as.

The constant moves onto an axis this collection does not measure. A table lookup and a sequence of shifts are both “constant time” and are not the same constant, and which is faster depends on the same cache-against-arithmetic ratio that decided every other trade of this shape here. What can be said is that the table is bits and the word operations are not.

And it explains why the two-layer structure is what gets described. The version with tables is the one that can be analysed by counting entries; the version with word tricks needs a word size in the statement, which is a parameter an asymptotic result would rather not have. The published bound describes the analysable structure and the shipped code is the other one, which is the same gap the threshold somebody chose records between a pseudocode and a library.

The number to carry

2.85 times smaller than a segment tree, and five times its own payload. Both halves of that sentence are the essay.

The first half is why the structure is worth building, and where a crossing moved to is what it does to the method it was built for. The second half is why “2n + o(n)” should be read as a statement about a limit rather than as a size — and why every plate in this strand draws the payload and the overhead as separate bars.

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.

Cartesian treeConstant factorDocument listingIndex sizeLookup tableMeasurementRange minimumRank directorySegment treeSelectSpace overheadSuccinctTrade off