The apparatus, three times smaller again
Three prices for the same capability, on sixty-four documents of two hundred and fifty-six characters.
The published apparatus — a document array, a previous-occurrence chain and a segment tree over the chain — is 838,797 bits beyond the suffix array and the text.
The chainless one, with the chain removed and a succinct range minimum in place of the segment tree, is 285,460.
The descent, with the document array in a wavelet tree and nothing else, is 119,972.
That is 34.0% and then 42.0%, or 14.3% end to end. The capability is unchanged: all three report the same documents for the same query, checked on the documents rather than on their count.
Where the bits were
The published apparatus’s 838,797 bits split four ways and the split is lopsided.
The chain is n⌈log₂ n⌉ bits — one row index per row, the same width as the suffix array itself. On this collection that is 16,449 rows at fifteen bits, or 246,735 bits.
The segment tree over the chain is the largest single part. A segment tree storing a minimum per node over n leaves is about 2n values of ⌈log₂ n⌉ bits, so roughly twice the chain: 493,470.
The document array is n⌈log₂ d⌉ — 16,449 rows at six bits, 98,694 bits.
So the chain and its index were 88% of the apparatus and the array was 12%.
That proportion is the reason the strand went in the order it did, and it is worth noticing that the order was not chosen — each removal was found when it was found. It happens that the largest parts went first.
There is a reason the segment tree is twice the chain rather than a fraction of it, and it is worth a sentence because succinct range minima are usually introduced as though the ordinary version were merely a little large. A segment tree over n leaves has n − 1 internal nodes, each holding a minimum, and each minimum is a value from the array — so a full-width value, ⌈log₂ n⌉ bits. Two n-sized arrays of full-width values.
The succinct alternative holds a Cartesian tree in 2n + o(n) bits — two bits a leaf rather than two values a leaf — which is a factor of ⌈log₂ n⌉/2, or seven and a half at this size. The shape a range question is about is where that representation was built here, and its measured gain on this part was 3.19 rather than 7.5, because the o(n) is not small at these sizes.
What each removal was worth
The chain, and its index. Removing the chain removes 246,735 bits directly. It also changes what the range structure is over: what the chain cost measured the pair and found the removal worth a factor of 1.93 on the whole apparatus, on top of the succinct range minimum’s 3.19 on its own part. Together: 838,797 to 285,460.
The range minimum entirely. The tree answers the question removes it, because the distinct symbols of an interval are what a wavelet tree enumerates. That is the whole remaining range structure gone — on this collection, 285,460 minus the array’s 98,694 minus the bitmap, so about 186,000 bits.
The array, compressed. The wavelet tree over the document array costs its entropy plus directories. On a collection of equal-length documents that is the plain array plus 20%; on a real one it is 83% of plain plus 20%, so about par. On this collection the tree is 119,972 including directories against a plain array of 98,694 — larger than the plain array.
That last line is the one worth reading twice. The final step made the apparatus 42% of what it was, and it did so entirely by removing the range minimum. The array itself got bigger.
That the array got bigger on this collection is a property of the collection rather than of the representation, and it is worth being precise. The documents here are equal length, so the array’s entropy is exactly log₂ d and the tree’s payload is exactly the plain array’s size. The directories are pure addition.
On a collection whose lengths fall as one over rank the payload would be 83% of plain and the total about par. On one with a dominant document it would be 40% and the total half. So the final step is a wash, a small win or a large one depending on a property of the collection that has nothing to do with the query — and this essay’s collection is the worst case for it.
Reporting the 42% on the worst case is the right choice and it should be said out loud: on a collection whose array compresses, the last step is worth more than 42%.
The step that costs and pays at once
The wavelet tree is a worse representation of the array considered as an array, and a better one considered as an apparatus. Separating those is the whole of the accounting.
As an array: plain costs n⌈log₂ d⌉ = 98,694 bits and supports a lookup in one read. The tree costs its entropy — here equal to n log₂ d, because the documents are equal length — plus rank directories at 20% of that, so 119,972, and supports a lookup in log d ranks.
Strictly worse, in both currencies, for the job of being an array.
As an apparatus: the tree also answers which distinct symbols are in this interval, which is the listing query, at a cost proportional to the answer. The plain array cannot answer that without reading every entry in the range, which is the naive method.
So the tree is buying the query with bits and with per-lookup cost, and the thing it is buying it from is the range minimum, which cost 186,000 bits. Net: 66,000 bits saved and the array’s lookups made log d times more expensive.
That is a real trade rather than a free improvement, and quoting the 42% without it would be quoting a saving that has a cost attached.
There is a smaller trade inside that one which is easy to miss. The chainless method needed a bitmap — one bit per document, cleared between queries — and the descent does not. That is d bits of working memory, negligible at sixty-four documents and not at two thousand, and it came with a trap: clearing the bitmap by walking it costs d per query, which on a query whose answer is three is worse than the array it replaced. A document already in the answer is where that trap is written out.
So the descent removes a structure, a bitmap and a clearing problem, and none of the three appears in the bar chart because two of them are query-time rather than resident.
What is left
The 119,972 bits are three parts and the shares are stable across collection shapes.
The payload — the tree’s levels, one bit per symbol per level of its code — is about 80%. That is where the length distribution’s entropy lives and where any further compression has to come from.
The rank directories are about 20%. They are a function of the levels’ total length rather than of their contents, which is why the share barely moves: 19.5%, 20.7%, 20.9% and 23.3% across the four collection shapes measured.
The code table is one code length and one symbol per document, so d(2⌈log₂ d⌉) bits. At sixty-four documents that is 768 bits — under one per cent, and it stops being negligible somewhere in the thousands.
There is no fourth part, which is what “the apparatus is one array” means.
Against the base it sits on
Every number above is bits beyond the suffix array and the text, and that base is what decides whether any of it matters.
On this collection the suffix array is n⌈log₂ n⌉ = 246,735 bits and the text is n⌈log₂ σ⌉ = 82,245, so the base is 328,980.
The published apparatus was 2.55 times its base. That is the number the apparatus that is smaller than its index is named after: an apparatus larger than the index it sits on is not an optimisation, it is a second index.
The chainless one is 87% of its base. The descent is 36.5%.
So the arc is a structure going from two and a half times its base to a third of it. That is the honest headline, and it is a better one than the 14.3%, because a share of an earlier version of oneself is a share of something a reader has no independent feel for.
The arc is also worth reading as four numbers rather than three, because the first structure this problem had is not on the chart. Reading every occurrence needs no apparatus at all: the document array alone, at 98,694 bits, and a scan of the range. That is the cheapest structure by a wide margin and it has the wrong asymptotic behaviour.
So the sequence is 98,694 for the naive method, 838,797 for the published output-sensitive one, 285,460 after two removals and 119,972 after the third. The apparatus started at eight and a half times the naive structure and is now at 1.22 times it — which is a fair summary of what three passes of removal achieved and is not the summary any of the three essays gave.
The base is the wrong base
There is a caveat that undermines every share in this essay and it should be stated rather than left for a reader to find.
The base is an uncompressed suffix array and an uncompressed text. Nobody builds that. The whole point of the surrounding field is that the base can be a self-index — the index that is smaller than the text — which on this collection would be around 6.4 bits a character, or 105,000 bits, against the 328,980 above.
Against that base the descent’s apparatus is not 36.5%; it is 114%. Still larger than the thing it sits on.
That changes the conclusion of this essay and it does not change the arc. Three removals took the apparatus from 800% of a self-index to 114% of one, which is a factor of seven and is worth having. It is not the “a third of the index” a reader would take away from the plate, because the plate’s base is what the earlier essays used and consistency with them was the reason to keep it.
The honest summary is two numbers: 36.5% of an uncompressed base and 114% of a compressed one, with the second being the one that describes a system anybody would build.
Three ways to quote one number
This essay has now quoted the same structure five ways and every one of them is defensible, which is a problem worth naming rather than resolving quietly.
14.3% of the published apparatus. True, and it compares the structure against an earlier version of itself, which a reader has no independent feel for.
36.5% of an uncompressed suffix array and text. True, and the base is one nobody builds.
114% of a self-index over the same collection. True, and it is the number that describes a system, and it is not what any of the three essays reported.
1.22 times the naive method’s document array. True, and it is the comparison against the alternative rather than against a predecessor.
A wash on this collection and half on a skewed one. True of the last step alone.
Five numbers, one structure, and the spread between the smallest and the largest is a factor of eight. A sixth of what, exactly made the general argument in a different strand: a share carries what it is a share of, and one that does not is a number about nothing.
The rule that comes out of having done it wrong here is narrower than that. When a strand improves a structure over several passes, the share against the previous version is the one that gets quoted and the share against the alternative is the one that matters. Every essay in this strand quoted the first, and the second is the one that would have said, three passes ago, that the method was still losing.
What would move it further
Three things could, and they are of decreasing plausibility.
A compressed vector representation of the levels. On a collection whose array has a dominant symbol this is worth a quarter — 1.976 bits a symbol to 1.571 on the dominated collection. On an even collection it is worth four per cent. It costs several times the work per rank and the directory grows as a share.
A different code shape. The tree’s payload is the weighted path length of its code, and Huffman minimises that among prefix codes. Nothing beats it here except giving up the prefix property, which gives up random access.
A different formulation. An inverted index over documents answers this query without a suffix array, a document array, or any of it — for indexed terms only. That is not a smaller version of this apparatus; it is a different design whose costs are not comparable, and it is not measured here.
None of the three removes an array, because there is one array and it is the input.
What the sizes do not say
Sizes are one axis and this strand has been measured on two, so it is worth putting the other one beside these numbers before drawing any conclusion from them.
The published apparatus costs two hundred and sixty-seven operations inside the structure to report eight documents. The chainless one costs eighty-three. The descent costs thirty-two. So the operation count fell by a factor of eight over the same three passes that took the size down by a factor of seven, which is a happier outcome than a strand of this shape usually gets — a size reduction that also reduces work is the exception rather than the rule.
But the units are not the same. A segment-tree node visit and a bit-vector rank on a compressed level are different amounts of real work, and this collection charges both as one operation. The comparison that is not one comparison is the essay that made that distinction the site’s own habit, and it cuts against the descent here: a rank is the more expensive of the two.
So the honest statement about the query is a factor of eight in operations and something smaller in time, and the difference between those two numbers is not measured anywhere in this strand.
One number that did not move
Across all three passes one quantity stayed exactly where it started, and it is the one that decides whether any of this is used.
The crossing — the density at which an output-sensitive method beats reading every occurrence — fell from 10.8 occurrences a document to somewhere around three and a half. The collections this field has, measured on every shape available, sit at 1.84 to 1.88.
So the apparatus went from losing by a factor of six to losing by a factor of two, and it is still losing. Seven times smaller, eight times fewer operations, and the naive method still wins on every corpus in hand.
That is not a reason to undo the work: a factor of six becoming a factor of two is most of the distance, and one more improvement of the same size would cross it. It is a reason to be careful about what the three essays mean together. Each of them reported a real gain against the previous structure and none of them reported the comparison that decides.
The shape of a strand that ends
An apparatus that shrank by a factor of seven over three passes did not need a new idea about document listing. Each pass found that something already present answered a question an extra array had been built for: the traversal order, then a better representation, then a neighbouring structure.
What that means for a reader with a different apparatus is a short checklist rather than a technique. For each derived array, ask whether the traversal knows, whether the representation can be tighter, and whether something else present already answers the question. Three for three here.
And when the checklist runs out, the strand is over — not because nothing more is possible, but because what remains is the input and a representation of it, and neither of those is machinery to be removed.
Named alongside this one
Essays reaching for the same objects. Nobody chose these; they are what the concept index makes visible.
- The array the walk never reads document array · document listing · index size · previous occurrence chain · range minimum · suffix array
- A list of documents is not a list of occurrences document array · document listing · index size · range minimum · suffix array
- The cost that is the size of the answer document array · document listing · index size · range minimum · suffix array
- Two thousand documents of two hundred characters document array · document listing · index size · range minimum · suffix array
- One separator, or one for each document array · index size · suffix array · wavelet tree
- The array is the length distribution document array · document listing · index size · wavelet tree
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.
Document arrayDocument listingIndex sizePrevious occurrence chainRange minimumSpace accountingSuffix arrayWavelet tree