Abstract 1 Introduction 2 Main Ideas 3 Hierarchy of Blocks 4 Jiggly Block Tree 5 Substring Search 6 Construction References Appendix A Hierarchy of Blocks (Missing Proofs) Appendix B Jiggly Block Tree (Missing Proofs)

Compressed Index with Construction in Compressed Space

Dmitry Kosolobov ORCID St. Petersburg State University, Russia
Abstract

Suppose that we are given a string s of length n over an alphabet {0,1,,nO(1)} and δ is the string complexity of s, a known compression measure. We describe an index on s with O(δlognδ) space, measured in O(logn)-bit machine words, which can search in s any string of length m in O(m+(occ+1)logϵn) time, where occ is the number of occurrences and ϵ>0 is any fixed constant (the big-O in the space bound hides factor 1ϵ). Crucially, the index can be built in O(nlogn) expected time by one left-to-right pass on the string s in a streaming fashion with O(δlognδ) construction space. The index does not use the Karp–Rabin fingerprints, and the randomization in the construction time can be eliminated by using deterministic dictionaries instead of hash tables (with a slowdown). The search time matches currently best results and the space is almost optimal (the known optimum is O(δlognδα), where α=logσn and σ is the alphabet size, and it coincides with O(δlognδ) when δ=O(n/α2)). This is the first index that can be constructed within such space and with such time guarantees. To avoid uninteresting marginal cases, all above bounds are stated for δΩ(loglogn).

Keywords and phrases:
compressed index, pattern matching, string complexity, grammar, block tree
Copyright and License:
[Uncaptioned image] © Dmitry Kosolobov; licensed under Creative Commons License CC-BY 4.0
2012 ACM Subject Classification:
Theory of computation Design and analysis of algorithms
Related Version:
Full Version: https://arxiv.org/abs/2602.13735 [38]
Funding:
Supported by the Russian Science Foundation (RSF), project 24-71-00062.
Editors:
Philip Bille and Nicola Prezza

1 Introduction

Given a string s of length n over the alphabet {0,1,,σ1} with σnO(1), a compressed index is a data structure that stores s in a compressed form supporting fast substring search. Currently best indexes use O(δlognδα) space [34, 35, 32, 33], measured in O(logn)-bit machine words, where α=logσn and δ is a compression measure called string complexity [47], and they can search a substring of length m in O(m+(occ+1)logϵn) time, where occ is the number of occurrences and ϵ>0 is any fixed constant (the big-O in the space bound hides factor 1ϵ). This space O(δlognδα) is known to be tight for a wide range of parameters [32, 33] and it lowerbounds the space of most dictionary-based compressed indexes such as indexes on the Lempel–Ziv parsing [16, 52], run-length grammars [17, 31], Burrows–Wheeler transform [24], and string attractors [29] (see [43] for an overview).

There are essentially two primary methods to achieve O(mlogn) search time within the space O(δlognδ), which both enhance the basic idea of Claude and Navarro [18]: (1) a neat scheme [24] first outlined by Gagie et al. [23] that combines the Karp–Rabin hashing [22, 27] and z-fast tries [4, 5]; (2) a variant of the so-called locally consistent parsing [17, 19, 26, 40]. Known indexes that achieve O(m+logϵn) search time combine both approaches [17, 33]. Method (1) is applicable for many indexes; unfortunately, it has a serious drawback: the Karp–Rabin hashing must be collision-free for all substrings of length 2k, for k=1,2,, and the only known algorithm that verifies this condition uses O(n) space and O(nlogn) time [10], which is prohibitive in use cases where the uncompressed data barely fits into memory. This issue hinders a wider usage of such indexes and it is the reason for the scarcity of experimental studies that try to translate the theoretical state-of-the-art methods to practice (we note that there are many works on construction of indexes, e.g., [1, 20, 28, 37, 50, 51], but they never use the collision-free Karp–Rabin hashing as the best theoretical results do).

We make a step forward to solve this problem by presenting an index with O(δlognδ) space and O(m+(occ+1)logϵn) search time that can be built in O(nlogn) expected time by one left-to-right pass in a streaming fashion with O(δlognδ) construction space. We do not use the Karp–Rabin hashes and the randomization in the construction can be removed using deterministic dictionaries instead of hash tables (with a slowdown by an O(lognloglogn) factor, see [3, 48]). The result is purely theoretical due to large constants under the big-O but, we believe, its ideas might inspire practical designs. We conjecture that the index can be further improved to O(δlognδα) space (also the construction space) and O(m/α+(occ+1)logϵn) time in the string packed model (see [11, 41]); this direction is left for a future work.

Throughout, s is the input string of length n with letters s[0],s[1],,s[n1] from an alphabet {0,1,,nO(1)}. For i,j, let s[i..j] be the substring s[i]s[i+1]s[j] (empty if i>j). For sequence t=t0,t1,,tm1 (maybe a string), denote by t=tm1,,t1,t0 its reversal. Denote |t|=m, s[i..j)=s[i..j1], [i..j]={i,i+1,,j}, [i..j)=[i..j1].

The string complexity, δ, for s is defined as δ=max{dk(s)/k:k[1..n]}, where dk(s) denotes the number of distinct substrings of s of length k [35, 47].

We heavily use the concept of blocks: a block B for string s is an object attached to a certain substring s[i..j]; denote 𝖻(B)=i and 𝖾(B)=j. The blocks are not “fragments”: two distinct blocks B and B can be attached to the same substring s[i..j]. If unambiguous, we treat a block B as a string, writing |B|, B=s[i..j] (with not necessarily i=𝖻(B)), etc. Two blocks B and B are equal as strings if s[𝖻(B)..𝖾(B)]=s[𝖻(B)..𝖾(B)]. When defining a new block, we usually use words like “generate”, “create”, “copy” (when the new block is a copy of an old one) and it must be clear to which substring the new block is attached. We sometimes treat the concept of blocks loosely but, hopefully, it will be clear from the context.

2 Main Ideas

What follows is a high level description of main ideas behind our index. Details and precise definitions follow. We first focus on a static index. Its construction algorithm is in Section 6.

Recent results on block trees [34, 44] showed that the space O(δlognδ) for indexes is surprisingly easy to obtain by, roughly, the following scheme (or its variants): one constructs a hierarchy of blocks with logn levels such that the level-k blocks are n/2k substrings s[i2k..(i+1)2k) of length 2k (the rightmost block might be shorter) and, for k>0, each level-k block has two child blocks from level k1: its prefix and suffix of length 2k1, respectively; then, each block B=s[i2k..(i+1)2k) for which the substring B¯=s[(i1)2k..(i+2)2k) has an occurrence at a position smaller than (i1)2k is encoded by a pointer to the leftmost occurrence of B in s and all descendants of B in the hierarchy are removed (for precise and detailed definitions, see [6, 7, 34, 42, 44]). The result is a block tree [6, 42].

After a closer inspection of the proof from [35] of the bound O(δlognδ) for the block tree size, one can notice that the bound still holds if the block boundaries are slightly “jiggled”. Our idea is to construct a “jiggy block tree”, in which level-k blocks are not of exact size 2k but somewhat close to this “on average”, and each block might have up to O(loglogn) children. The variability of block lengths will be used to form a kind of locally consistent parsing, more specifically, we will use a “cut” version of the so-called deterministic coin tossing (DCT) [19] and the blocks will be built level by level bottom-up using this “cut DCT” (the idea of such level-by-level construction itself is not novel [46] but it is usually used with the full DCT and it leads to a grammar of size Ω(δlognδlogn), not to a kind of block tree). Then, the same arguments as in [35] show that the resulting tree has O(δlognδ) internal nodes. However, some nodes might have up to Θ(loglogn) leaf children, blowing up the space by an Ω(loglogn) factor, which seems unavoidable when using the DCT [12, 39, 40, 46, 49, 50].

We eliminate the factor Ω(loglogn) by introducing a novel intermediate step in the level by level bottom-up construction: given a block B with O(loglogn) children Bi,,Bj, we greedily unite the children from left to right into larger “intermediate blocks” B=BmBm+r1 such that the sequence of blocks BmBm+r1 occurs somewhere to the left in the block hierarchy; then, we store in the intermediate block B a pointer to this earlier occurrence. Due to the property of “local consistency” of DCT, the pointers will be somewhat “aligned” on the block structure and our jiggly block tree will resemble a grammar with elements of the block tree structure (like a collage system [30] but more structured). There are many details in this scheme to make it efficient that are explained in the sequel.

For the pattern matching, we follow the standard approach based on the locally consistent parsing and z-fast tries [17, 33] but we replace the Karp–Rabin fingerprints [27] with new deterministic fingerprints that are derived from our locally consistent block structure.

3 Hierarchy of Blocks

At the core of our index is a hierarchy of blocks (see Fig. 1, the leftmost picture): it has O(logn) levels where the topmost level has one block equal to s and the bottom level 0 contains n one-letter blocks s[0],,s[n1]; the concatenation of all blocks from one level equals s; for >0, each level- block B either is a copy of a level-(1) block B with 𝖻(B)=𝖻(B) and 𝖾(B)=𝖾(B) or is the concatenation of corresponding consecutive level-(1) blocks. We define the hierarchy of blocks by a bottom-up construction process.

The bottom level-0 blocks are n one-letter blocks s[0],s[1],,s[n1]. On a generic step with k0, we have a sequence of blocks B1,,Bb of level 2k whose concatenation is s itself. To produce new blocks of levels 2k+1 and 2k+2, the construction process will unite some adjacent blocks or will copy some blocks unaffected; in particular, all blocks of length >2k are copied to levels 2k+1 and 2k+2 unaffected. Every block B is assigned an O(logn)-bit integer identifier 𝗂𝖽(B); the level-0 blocks have identifiers equal to the letters they represent (recall that the alphabet is {0,1,,nO(1)}). Blocks with equal identifiers are equal as strings but the converse is not necessarily true. We choose an arbitrary unambiguous method to encode any pair of O(logn)-bit integers x,y into one O(logn)-bit integer, denoted x,y. The process maintains a counter to assign new unique identifiers to new blocks.

Level 𝟐𝒌+𝟏.

In the level-2k blocks B1,,Bb, we identify all maximal “runs” of blocks Bi,Bi+1,,Bi+r1 with equal identifiers such that |Bj|2k, for j[i..i+r), and i=1 or 𝗂𝖽(Bi1)𝗂𝖽(Bi), and i+r1=b or 𝗂𝖽(Bi+r)𝗂𝖽(Bi+r1). Each such run for which r>1 is substituted by a new block B=BiBi+1Bi+r1 whose identifier encodes the pair 𝗂𝖽(Bi),r. Hence, any two distinct runs of the same length r>1 with the same identifiers 𝗂𝖽(Bi) of the run blocks are substituted by blocks with the same identifier 𝗂𝖽(Bi),r. Thus, the blocks for level 2k+1 consist of copies of blocks from level 2k with length >2k, copies of blocks that formed runs of length one, and the blocks substituted in place of longer runs.

Level 𝟐𝒌+𝟐.

For any distinct integers x,y0, denote by 𝖻𝗂𝗍(x,y) the index (0-based) of the lowest bit in which the bit representations of x and y differ; e.g., 𝖻𝗂𝗍(8,0)=3. Denote 𝗏𝖻𝗂𝗍(x,y)=2𝖻𝗂𝗍(x,y)+a, where a is the bit of x with index 𝖻𝗂𝗍(x,y). Cole and Vishkin based their deterministic coin tossing (DCT) technique on the following observation.

Lemma 1 (see [19, 39]).

Given a string a0a1am over an alphabet [0 .. 2u) such that ai1ai for any i[1..m], the string b1b2bm such that bi=𝗏𝖻𝗂𝗍(ai1,ai), for i[1..m], satisfies bi1bi, for any i[2..m], and bi[0 .. 2u), for any i[1..m].

Let B1,,Bb be all blocks on level 2k+1. For each Bi, we assign two identifiers, denoted 𝗂𝖽(Bi) and 𝗂𝖽′′(Bi). If |Bi|>2k, define 𝗂𝖽(Bi)=. If |Bi|2k, then 𝗂𝖽(Bi1)𝗂𝖽(Bi) since the previous stage united all runs; define 𝗂𝖽(Bi)=𝗏𝖻𝗂𝗍(𝗂𝖽(Bi1),𝗂𝖽(Bi)) if i>1 and |Bi1|2k, and 𝗂𝖽(Bi)= otherwise. Define 𝗂𝖽′′(Bi)=𝗏𝖻𝗂𝗍(𝗂𝖽(Bi1),𝗂𝖽(Bi)) if i>1 and 𝗂𝖽(Bi1) and 𝗂𝖽(Bi), and define 𝗂𝖽′′(Bi)= otherwise. This computation of 𝗂𝖽′′ corresponds to two first steps of the standard DCT, i.e., it is a “cut” variant of DCT.

For any maximal range of blocks Bp,,Bq such that |Bh|2k for each h[p..q] (i.e., p=1 or |Bp1|>2k, and q=b or |Bq+1|>2k), we have 𝗂𝖽′′(Bp)=𝗂𝖽′′(Bp+1)= (or just 𝗂𝖽′′(Bp)= if p=q) and, by Lemma 1, for each h[p+2..q], we have 𝗂𝖽′′(Bh1)𝗂𝖽′′(Bh) and 0𝗂𝖽′′(Bh)O(loglogn) (here we used the assumption 𝗂𝖽(Bh)nO(1)). We will split Bp,,Bq into ranges ending at points corresponding to local minima of 𝗂𝖽′′. To this end, we mark each block Bi, with i[1..b], for which one of the following conditions holds:

  1. (a)

    |Bi|>2k or |Bi+1|>2k or i=b (the last block on level 2k+1);

  2. (b)

    i>2 and >𝗂𝖽′′(Bi2)>𝗂𝖽′′(Bi1) and 𝗂𝖽′′(Bi1)<𝗂𝖽′′(Bi)< (i.e., 𝗂𝖽′′(Bi) is immediately preceded by a local minimum).

The marking splits all blocks B1,,Bb on level 2k+1 into disjoint ranges as follows: each range Bi,,Bj consists of unmarked blocks Bi,,Bj1 and the marked block Bj and either i=1 or Bi1 is marked. For each such range Bi,,Bj: if i=j, the process copies Bi to level 2k+2; if i<j, it creates a new block B=BiBj for level 2k+2 assigning to it a new identifier unless another block B produced by the same sequence 𝗂𝖽(Bi),,𝗂𝖽(Bj) has already appeared earlier in the process (on any level), in which case we assign 𝗂𝖽(B)=𝗂𝖽(B). Since 𝗂𝖽′′O(loglogn), local minima for 𝗂𝖽′′ occur often and, thus, jiO(loglogn).

Properties of blocks.

The main properties of the hierarchy are its “local consistency” and “local sparsity”. The proof of the former is standard [35] and it was moved to Appendix A.

Lemma 2 (local consistency).

Fix =2k or =2k+1. Let B1,B2,,Bb denote all level- blocks. For any i, j and block Bh, if s[i2k+4..j+2k+4]=s[𝖻(Bh)2k+4..𝖾(Bh)+2k+4], then there exists a level- block Bh such that 𝗂𝖽(Bh)=𝗂𝖽(Bh), 𝖻(Bh)=i, and 𝖾(Bh)=j.

We call a position h[0..n) a block boundary for level if h=𝖾(B) for some level- block B. The “local sparsity” formalizes the intuition that blocks on levels 2k and 2k+1 have length 2k “on average”. The proof is quite technical and it mostly repeats the arguments from the proof of [39, Lemma 11] word by word. The proof can be found in the full version of the paper [38, Appendix A].

Lemma 3 (local sparsity).

For any i<j, the number of block boundaries in [i..j) on levels 2k and 2k+1 is at most 26ji2k. In particular, there are O(n/2k) blocks on these levels.

Lemma 3 implies that there are O(logn) levels in the hierarchy. We conclude the section with a technical lemma. Its proof is quite straightforward and has been moved to Appendix A.

Lemma 4.

Any blocks B and B with 𝗂𝖽(B)=𝗂𝖽(B) are equal as strings and both were created on the same level.

Figure 1: A schematic depiction of the hierarchy of blocks H. Colored rectangles with the same color depict blocks with the same 𝗂𝖽; white rectangles are blocks with any 𝗂𝖽. From left to right: the hierarchy H, H with rules 1–2 applied, H with rule 4 applied, H with rule 3 applied (i.e., it is H^).

4 Jiggly Block Tree

The described hierarchy of blocks for s naturally forms a tree H with blocks as vertices in which the root is the (only) topmost block and the parent of any level- block B with Bs is the level-(+1) block B such that [𝖻(B)..𝖾(B)][𝖻(B)..𝖾(B)] (note that B and B may be equal as strings). For our index, we create a tree H^ that is a copy of H with vertices removed according to the following rules 1–4 while it is possible to apply them (see Fig. 1):

  1. 1.

    if a level-0 block B has parent B with 𝗂𝖽(B)=𝗂𝖽(B), merge B and B by removing B and connecting B to the parent of B;

  2. 2.

    if, for >0, a level- block B has parent B with 𝗂𝖽(B)=𝗂𝖽(B), merge B and B by removing B and connecting all children of B to B;

  3. 3.

    if, for >0 and >0, a level- block B has a “preceding” level- block B such that 𝗂𝖽(B)=𝗂𝖽(B) and 𝖻(B)<𝖻(B), remove all descendants of B;

  4. 4.

    if, for >0, all children of a level- block B have equal 𝗂𝖽, remove them all except for the leftmost child (note that, by removing a child, we remove its descendants).

The idea of such construction is not novel (however, usually the full DCT is used, not the “cut” DCT as we do) and it leads to an index with ω(δlognδ) space [12, 39, 40, 46, 49, 50]. As it turns out, the obtained tree has O(δlognδ) non-leaf vertices and the main issue with space is due to some vertices having up to O(loglogn) leaves from levels 2k+1. To reduce the space, our idea is to parse the sequences of leaves by a specialized variant of LZ77 [52]. The construction is quite technical and requires some preparations to define it.

Let B1,,Bb be all blocks on a level 2k+1. Recall that the block construction process has marked some of the blocks, thereby splitting them into ranges Bi,,Bj, where Bi,,Bj1 are unmarked and Bj is marked. Fix such range Bi,,Bj with i<j such that Bi,,Bj were not removed from H^ after the rules 1–4 above. Let B be the parent of Bi,,Bj. We are to parse Bi,,Bj into subranges. To define the parsing, we need some notation.

For any block Bm and 0, choose the largest h such that, for all h[mh..m), Bh is not marked; define 𝗅𝖾𝖿𝗍(m,) as the sequence β,𝗂𝖽(Bmh),𝗂𝖽(Bmh+1),,𝗂𝖽(Bm1), where β is empty (i.e., it is really not in the sequence) if h=, and β is a special symbol $ if h<. Choose the largest h such that, for all h[m..m+h1), Bh is not marked (but Bm+h1 may be marked); define 𝗋𝗂𝗀𝗁𝗍(m,) as the sequence 𝗂𝖽(Bm),𝗂𝖽(Bm+1),,𝗂𝖽(Bm+h1).

We greedily parse Bi,,Bj into subranges from left to right: suppose that Bi,,Bm1 have already been parsed (initially m=i), let Bm,,Bm+r1 be the longest subrange with m+r1j for which there is m<m such that 𝗅𝖾𝖿𝗍(m,4r)=𝗅𝖾𝖿𝗍(m,4r) and 𝗋𝗂𝗀𝗁𝗍(m,5r)=𝗋𝗂𝗀𝗁𝗍(m,5r) (note that 𝗂𝖽(Bm),,𝗂𝖽(Bm+r1) must be a prefix of 𝗋𝗂𝗀𝗁𝗍(m,5r)); let r=1 if there is no such Bm,,Bm+r1; once r is determined, we have obtained a new subrange Bm,,Bm+r1 and we continue the parsing for Bm+r,,Bj, assigning m:=m+r.

Define a new tree J, called the jiggly block tree, that is a copy of H^ with the following modifications. For each level 2k+1, we consider each range Bi,,Bj as described above such that all Bi,,Bj were not removed from H^ after the rules 1–4, and we parse Bi,,Bj into subranges as described above; for each subrange Bm,,Bm+r1 with r>1, we remove from J the blocks Bm,,Bm+r1 (which are children of B) and insert in their place a new block B with parent B and no children; B is called intermediate. We do not assign 𝗂𝖽 to B.

By a simple analysis, one can show that any “original” block B of H cannot disappear from J, always a copy of B remains, which is formalized as follows (the proof is in Appendix B).

Lemma 5.

For d0, let B be the leftmost topmost block with 𝗂𝖽(B)=d and |B|>1 in the hierarchy H, i.e., 𝖻(B) is minimal for B among all blocks with this 𝗂𝖽 and B is such block with maximal level. The jiggly block tree J retains the block B and B has children in J.

To support the basic navigation on the string s, the tree J is equipped as follows. Each non-intermediate block B in J stores 𝖻(B), 𝖾(B), 𝗂𝖽(B), pointers to children of B, and the lowest level (B) on which a block with identifier 𝗂𝖽(B) was created in H. For each non-leaf block B in J, two cases are possible: (1) B has children Bi,,Bj in J (some of which might be intermediate blocks inserted in place of old children of B from H) whose concatenation, if viewed as strings, equals s[𝖻(B)..𝖾(B)] (i.e., no “gaps” between the children); (2) B has only one child B and all removed children had the same identifiers 𝗂𝖽(B), i.e., B was a run block with exactly |B|/|B| children in H. If a block B in J with |B|>1 has no children, then either B is intermediate or, by Lemma 5, there is a block B with children in J such that 𝗂𝖽(B)=𝗂𝖽(B). In the latter case, we store in B a pointer to B; observe that if B is from a level , then all children Bi,,Bj of B are from levels 1 since, by Lemma 4, B and B originate from blocks created on the same level in the hierarchy H. Hence, when one moves from the level- block B to B and, then, to a child of B, the level decreases to at most 1. Thus, if there are no intermediate blocks in J, one can read s[𝖻(B)..𝖾(B)] for any block B in J in O(|B|) time by naively descending in J as in grammar indexes [17].

Consider an intermediate block B in J from a level 2k+1. By construction, it has no children in J. Let B be its parent. Denote by B1,,Bb all blocks from level 2k+1 in the hierarchy H; we mark them according to conditions (a) and (b) from Section 3 so that we can use the notation 𝗅𝖾𝖿𝗍 and 𝗋𝗂𝗀𝗁𝗍 from now on. Suppose that B=BiBj, where Bi,,Bj is the range of blocks from which B was produced in H. Since B is intermediate, we have B=BmBm+r1, for some m and r such that [m..m+r)[i..j]. By construction, there is m<m with 𝗅𝖾𝖿𝗍(m,4r)=𝗅𝖾𝖿𝗍(m,4r) and 𝗋𝗂𝗀𝗁𝗍(m,5r)=𝗋𝗂𝗀𝗁𝗍(m,5r). Choose the smallest m′′ such that 𝗋𝗂𝗀𝗁𝗍(m′′,r)=𝗋𝗂𝗀𝗁𝗍(m,r). Obviously, m′′m. Let Bi,,Bj be a range of blocks in H such that im′′j, and let B^ be a block on level 2k+2 produced from this range. Since 𝗋𝗂𝗀𝗁𝗍(m′′,r)=𝗋𝗂𝗀𝗁𝗍(m,r), the definition of 𝗋𝗂𝗀𝗁𝗍 implies that the blocks Bm′′,,Bm′′+r2 are unmarked and, therefore, [m′′..m′′+r)[i..j]. Since m′′ is the smallest such index, B^ must be the leftmost block in H from level 2k+2 produced from the sequence 𝗂𝖽(Bi),,𝗂𝖽(Bj), which, by Lemma 4, implies that B^ is the leftmost block with the identifier 𝗂𝖽(B^) in the whole hierarchy H. By Lemma 5, the block B^ was retained in J and has children in J. We store in B a pointer to B^ and the following numbers required to identify the location of the copy of B=BmBm+r1 among the children of B^: 𝗈𝖿𝖿(B)=m′′i and 𝗋(B)=r. (Note that B^ might coincide with B, albeit it is unusual.)

Some subranges of blocks in the range Bi,,Bj could be replaced with intermediate blocks in J. We claim that any such intermediate block B^ that replaced some of the blocks Bm′′,,Bm′′+r1 has 𝗋(B^)r/2. Suppose, to the contrary, that B^=Bm^Bm^+r^1 is such that r^=𝗋(B^)>r/2 and [m^..m^+r^)[m′′..m′′+r). Since 4r^>r, the concatenation of the sequences 𝗅𝖾𝖿𝗍(m^,4r^) and 𝗋𝗂𝗀𝗁𝗍(m^,5r^) contains the sequence 𝗂𝖽(Bm′′),,𝗂𝖽(Bm′′+r1). Hence, by construction, there is m′′′<m′′ such that 𝗋𝗂𝗀𝗁𝗍(m′′′,r)=𝗋𝗂𝗀𝗁𝗍(m′′,r), which contradicts the minimality of m′′. This claim implies that, for an intermediate block B in J, one can compute all r children of B from the original hierarchy of blocks H in O(r) time by the simple traversal of pointers in J. Since, by Lemma 3 (local sparsity), any block B has O(k=1|B|/2k)=O(|B|) descendants in the hierarchy H, we obtain the following result.

Lemma 6.

Given a block B in the jiggly block tree J, one can obtain all r children of B from the original hierarchy of blocks H in O(r) time; further, one can compute all descendant blocks for B in H in O(|B|) time.

Theorem 7.

The jiggly block tree for string s of length n has size O(δlognδ), where δ is the string complexity of s, provided δΩ(loglogn).

Proof (sketch)..

We are to estimate the number of blocks in J as O(δlognδ). The basic argument is as in [35]: we will estimate the number of internal blocks on each level as O(δ), which implies that the number of internal blocks on 2lognδ lowest levels is O(δlognδ), as required; then, by Lemma 3 (local sparsity), the number of blocks on levels 2lognδ is at most O(k=log(n/δ)n/2k)=O(δ). This scheme will be complicated by the subsequent counting of leaves since in J a block may have up to O(loglogn) leaf-children.

Fix =2k+1 or 2k+2, for k0. Every non-leaf level- block B in J satisfies |B|2k and its corresponding substring s[𝖻(B)2k+4..𝖾(B)+2k+4] cannot occur at smaller positions since otherwise, due to Lemma 2 (local consistency), there is a copy of B to the left of B and, thus, all children of B should be removed and a pointer to this copy should be stored in B. We associate with every such B a group of Θ(2k) substrings of length 2k+6, each of which covers s[𝖻(B)2k+4..𝖾(B)+2k+4] and, hence, cannot occur at smaller positions, i.e., they are “leftmost substrings”. Due to Lemma 3 (local sparsity), at most O(1) level- blocks can be associated with one substring of length 2k+6. Therefore, Θ(2k)bO(d2k+6), where b is the number of non-leaf level- blocks B and d2k+6 is the number of distinct substrings of length 2k+6 (which is equal to the number of leftmost substrings of length 2k+6). Thus, we obtain bO(d2k+6/2k+6)O(δ). (The described argument is exactly the same as in [35].)

Figure 2: A schematic representation of a group of Θ(2k) leftmost substrings of length 2k (depicted as arcs above) associated with a leaf block B=BmBm+r1. The blue rectangles represent blocks Bm4r,,Bm+5r1 and their copies to the left, Bm4r,,Bm+5r1. The red rectangles represent that it is impossible to extend the subrange to Bm4(r+1),,Bm+5(r+1)1.

If every block in J had O(1) leaves, the total number of blocks would be O(number of non-leaf blocks). Unfortunately, some blocks may have up to O(loglogn) leaves. We analyze each level- leaf B that is not the rightmost child of its parent B. B was generated from a sequence of level- blocks: B=BiBj where a subrange Bm,,Bm+r1 generated the leaf block B (intermediate, if r>1, or not, if r=1). See Fig. 2. The subranges were produced by a greedy parsing that tried to produce the subrange Bm,,Bm+r instead of Bm,,Bm+r1 but failed since there were no occurrences of the sequence Bm4(r+1),,Bm+5(r+1)1 (the subrange with a “neighbourhood” of 4(r+1) blocks to its left and right) to the left in the hierarchy of blocks. Therefore, the substring s[a..b]=s[𝖻(Bm4(r+1))2k+4..𝖾(Bm+5(r+1)1)+2k+4] cannot occur at smaller positions since, otherwise, by Lemma 2 (local consistency), there would be an earlier occurrence for the sequence. We associate with B a group of Θ(2k) leftmost substrings of length 2k+1 that cover s[a..b], where 2k is the closest power of two to the length of s[a..b]. We consider separately each number k with all such groups of Θ(2k) leftmost substrings, and the further analysis to bound the number of these groups by O(δ), for this k, is similar as above, in principle, but it has many subtle details and special cases.

All remaining details of the proof were moved to Appendix B.

5 Substring Search

Let us augment the jiggly block tree J to support searching in s. Our approach is standard but with modifications since we will not use Karp–Rabin fingerprints [27]. Instead, we invent somewhat analogous deterministic fingerprints. Throughout, we use hash tables supporting queries in O(1) time [9]. Let us fix a string t of length m, the pattern to search.

Parsing 𝒕.

We create a compacted trie T𝗂𝖽 and, for each non-run block B in J that was created from a range of blocks Bi,,Bj from a level 2k+1 of the hierarchy H, we store in T𝗂𝖽 the sequence 𝗂𝖽(Bi),,𝗂𝖽(Bj) (treated as a string with letters 𝗂𝖽(Bi),,𝗂𝖽(Bj)). As is usual for compacted tries, the edge labels in T𝗂𝖽 are sequences of 𝗂𝖽s and there is a node x such that 𝗌𝗍𝗋(x)=𝗂𝖽(Bi),,𝗂𝖽(Bj), where 𝗌𝗍𝗋(x) denotes the sequence written on the root–x path; we store in x a pointer to B. The edges of T𝗂𝖽 do not store their edge labels, only the lengths of these edge labels. Each internal node of T𝗂𝖽 is augmented with a hash table that maps any block identifier to the child whose edge label starts with this identifier (if any). It follows from Theorem 7 that the size of T𝗂𝖽 is O(δlognδ).

Our search algorithm constructs a hierarchy of blocks for t analogous to H. Level 0 in the hierarchy for t contains the one-letter blocks t[0],,t[m1] whose identifiers are the corresponding letters; on a generic step of the algorithm, for 0, we have all level- blocks B1,,Bb and B1Bb=t. Suppose that =2k, for some k0. As in Section 3, we identify in O(b) time all maximal runs of blocks Bi,,Bi+r1 with equal identifiers (i.e., 𝗂𝖽(Bi)==𝗂𝖽(Bi+r1)) such that |Bi|2k. If r=1 or |Bi|>2k, we copy the block Bi to level 2k+1. If r>1, we substitute such run Bi,,Bi+r1 with a new block B for level 2k+1 with identifier 𝗂𝖽(Bi),r. Thus, all block for level 2k+1 are constructed. Suppose that =2k+1, for some k0. As in Section 3, we assign in O(b) time to each block Bi an identifier 𝗂𝖽′′(Bi) that is either (for example, if |Bi|>2k) or an integer that is at most O(loglogn); we then produce new blocks for level 2k+2 by identifying local minima for 𝗂𝖽′′ and values of 𝗂𝖽′′ equal to ; we omit details as they are the same as in Section 3. It remains to assign identifiers to new blocks. Suppose that a new such block B was created from blocks Bi,,Bj from level 2k+1. We descend from the root of the trie T𝗂𝖽 reading the sequence 𝗂𝖽(Bi),,𝗂𝖽(Bj) and skipping sequences written on the edge labels; thus, we reach a node x that stores a pointer to a block B in J and we retrieve all skipped edge labels in O(ji+1) time traversing J from B as described in Lemma 6; we then assign 𝗂𝖽(B)=𝗂𝖽(B). If the described search has failed at any stage, we assign to 𝗂𝖽(B) a new unique identifier (for consistency, we also should maintain another trie analogous to T𝗂𝖽 for new identifiers but, as it will be seen, this consistency is not very important).

The algorithm processes all b blocks on one level in O(b) time. It then follows from Lemma 3 (local sparsity) that the overall time is O(k=0m/2k)=O(m).

Deterministic fingerprints.

We view any substring t[p..q) as the sequence of level-0 blocks t[p],,t[q1] and we define its deterministic fingerprint as 𝖿𝗂𝗇(0,t[p],,t[q1]), where 𝖿𝗂𝗇(,B1,,Bb) is the following recursive procedure that assigns a fingerprint to any contiguous subsequence of level- blocks B1,,Bb from the hierarchy of blocks of t.

  1. 1.

    The fingerprint 𝖿𝗂𝗇(,B1,,Bb) of the empty sequence (b=0) is the empty sequence.

  2. 2.

    Case =2k, for k0. Choose maximal i0 such that |Bi|2k and 𝗂𝖽(Bi)=𝗂𝖽(Bh), for all h[1..i], or i=b. Choose minimal jb such that |Bj+1|2k and 𝗂𝖽(Bj+1)=𝗂𝖽(Bh), for all h(j..b], or j=0. If i=b, the fingerprint is 𝗂𝖽(B1),b. If ib, the fingerprint is the sequence 𝗂𝖽(Bi),i,𝖿𝗂𝗇(2k+1,B1,,Bb), 𝗂𝖽(Bj+1),bj, where the element 𝗂𝖽(Bi),i is missing if i=0, 𝗂𝖽(Bj+1),bj is missing if j=b, and B1,,Bb are all level-(2k+1) blocks that are parents of the blocks Bi+1,,Bj.

  3. 3.

    Case =2k+1, for k0. Redefine the identifiers 𝗂𝖽′′ for B1,,Bb as in Section 3. Choose the smallest i such that either |Bi+1|>2k (where 0i<b) or >𝗂𝖽′′(Bi2)>𝗂𝖽′′(Bi1) and 𝗂𝖽′′(Bi1)<𝗂𝖽′′(Bi)< (where 3ib); let i=b if there is no such i. Symmetrically, choose the largest j such that either |Bj|>2k or >𝗂𝖽′′(Bj2)>𝗂𝖽′′(Bj1) and 𝗂𝖽′′(Bj1)<𝗂𝖽′′(Bj)< (where 3jb); let j=b if there is no such j. Note that ij. The fingerprint is 𝗂𝖽(B1),,𝗂𝖽(Bi),𝖿𝗂𝗇(2k+2,B1,,Bb),𝗂𝖽(Bj+1),,𝗂𝖽(Bb), where B1,,Bb are all level-(2k+2) blocks that are parents of the blocks Bi+1,,Bj.

The indices i and j in the procedure 𝖿𝗂𝗇 are chosen to be at block boundaries for the blocks B1,,Bb from level +1 so that all children of B1,,Bb are exactly the blocks Bi+1,,Bj. Therefore, the concatenation of all blocks used in the fingerprint of t[p..q) is equal to t[p..q) (Fig. 3). Due to Lemma 3 (local sparsity), 𝖿𝗂𝗇 has at most O(logm) levels of recursion. Since 𝗂𝖽′′ is at most O(loglogn) whenever it is not , it follows from the discussion of Section 3 that case 3 in the recursion (when =2k+1) uses at most O(loglogn) blocks for the resulting fingerprint. Hence, the size of the fingerprint is O(logmloglogn).

Figure 3: Here s[p..q]=s[p..q]. The blocks depicted as blue form the fingerprints of these strings; the red blocks intersect the strings but can be “inconsistent” and are not in the fingerprints.

We store in each block B in the hierarchy built for t pointers to children/parent of B and to the first preceding block on the same level with different 𝗂𝖽(B). With this, one can retrieve the fingerprint for any substring of t in time O(logmloglogn).

Analogously, we define the deterministic fingerprint for any substring of s.

Lemma 8.

Two substrings of s or t coincide iff their deterministic fingerprints coincide.

Proof.

By construction, any two blocks in both hierarchies (for s and for t) that have equal identifiers are equal as strings. Therefore, two substrings with equal fingerprints must be equal. For converse, suppose that two substrings s[p..q) and t[p..q) are equal. It suffices to show that the procedure 𝖿𝗂𝗇 computing the fingerprints for s[p..q) and t[p..q) does the same calculations. Suppose that, on a generic step, a call to 𝖿𝗂𝗇(,B1,,Bb) occurs during the computation for s[p..q) and the same call 𝖿𝗂𝗇(,B1,,Bb) for t[p..q); initially, for =0, 𝖿𝗂𝗇(0,s[p],,s[q1]) and 𝖿𝗂𝗇(0,t[p],,t[q1]) are called on the same sequences of level-0 blocks. Cases 2 or 3 in the description of 𝖿𝗂𝗇 obviously produce the same sequences of identifiers surrounding a recursive call to 𝖿𝗂𝗇; the recursive call receives as its arguments a certain new sequence of blocks B1,,Bb. In case 2 this sequence B1,,Bb is the same in both calls since the blocks B1,,Bb either are copies of blocks from B1,,Bb or are runs of blocks to which we assigned the same identifiers of the form 𝗂𝖽(B),r in both hierarchies of blocks. In case 3 the argument is more complicated but still straightforward since the blocks B1,,Bb were produced from local minima of the function 𝗂𝖽′′ and we strategically skipped some blocks from the begin and end of the sequence B1,,Bb to make the local minima consistent, and, further, we rely on the fact that blocks created from the same sequences of blocks in both hierarchies are assigned with the same identifiers (recall that the trie T𝗂𝖽 is used to guarantee this). The values of 𝗂𝖽′′ for B1,,Bb during the computation of 𝖿𝗂𝗇 are exactly the same as they were in the definition of both hierarchies of blocks (for s and for t) except, possibly, for B1 and B2, as 𝗂𝖽′′(B1)=𝗂𝖽′′(B2)= when 𝗂𝖽′′ was computed in 𝖿𝗂𝗇. But a simple case analysis shows that this nuance does not hurt the argument.

By Lemma 6, the hierarchy of blocks H for s can be emulated using J, which suffices to get the fingerprint of any substring of s. But this approach is inefficient as it assembles the fingerprints top down, not bottom up as in the definition. We speed up the assembling by adding the following data structures. Let B be a block of J (intermediate or not) with |B|>1. The block B must have been produced from a run/range/subrange of blocks Bi,,Bj with i<j. We store in B a link to a block in J with identifier 𝗂𝖽(Bi), which exists in J by Lemma 5. The links naturally form a forest AL on all blocks. Define a symmetric forest AR for rightmost blocks (i.e., where the link from B refers to 𝗂𝖽(Bj)). We equip AL and AR with the weighted ancestor structure [25, 36] that, for any B and any threshold w, can find the farthest ancestor B^ of B in AL (or AR) with |B^|w in O(loglogn) time. Using AL and AR, we obtain the following result; its tedious proof is very technical and it can be found in the full version of the paper [38, Appendix C].

Lemma 9 (fingerprints).

Given a substring s[p..q] and the lowest block B in the hierarchy of blocks H for s with 𝖻(B)p<q𝖾(B), if J retains B and B has children in J, then one can compute the fingerprint of s[p..q] in O(logm(loglogn)2) time, where m=qp.

Indexing structures.

We use a well-known scheme for pattern matching. The following description is sketchy in parts as it is mostly the same as in [17, 18]. Given s[p..p+|t|)=t, let B be the lowest block in J such that 𝖻(B)p<p+|t|1𝖾(B). If B has children in J, we call the occurrence primary; otherwise, secondary. We first find primary occurrences.

Let T and T be two compacted tries. Consider each block B in J. Let B be a non-run block with children Bi,,Bj in J. For each h[i..j), we store the string Bh+1 in T and T has an explicit node x such that 𝗌𝗍𝗋(x)=Bh+1; we store BiBh in T with an explicit node y such that 𝗌𝗍𝗋(y)=BiBh. Thus, we produce pairs of nodes (x,y) associated with B and indices h. Let B be a run block whose only child in J is B1 and r=|B|/|B1|. We store the string B1 in T and B1r1 in T, thus producing a pair of nodes (x,y) associated with B. The edges in T and T store only lengths of edge labels. By Theorem 7, the size of T and T and the number of produced pairs (x,y) is O(δlognδ). Each node z in T stores a pointer to a block B in J such that 𝗌𝗍𝗋(z) is a prefix of B; each node z in T stores an index h and a pointer to B in J whose children are Bi,,Bj such that 𝗌𝗍𝗋(z) is a prefix of BiBh.

Define an order on nodes of T and T: x<x iff 𝗌𝗍𝗋(x)<𝗌𝗍𝗋(x). Fix a constant ϵ>0. We store a range reporting structure R [15]: it contains all produced pairs of nodes (x,y) in O(δlognδ) space (the big-O hides factor 1ϵ) and, given any nodes a,b of T and c,d of T, we can report all N pairs (x,y) such that axb and cyd in O((1+N)logϵn) time.

We turn T into a z-fast trie by augmenting it with a hash table that, for each node x, maps the fingerprint of a certain prefix of 𝗌𝗍𝗋(x) to x. We use our deterministic fingerprints. Since each fingerprint takes ω(1) space, the hash table is organized as a compacted trie Tf containing the fingerprints, treated as sequences of identifiers of length O(logmloglogn); the edges of Tf do not store their edge labels, only lengths (as in the trie T𝗂𝖽). Let t[q..m) be a string that can be read by descending from the root of T, assuming that the edge labels are somehow accessible by an “oracle”. The z-fast trie can read it in O(log2mloglogn) time by querying the hash on O(logm) fingerprints of prefixes of t[q..m). If t[q..m) cannot be read from the root of T, the z-fast trie returns an arbitrary node x of T. The found node x contains a pointer to a block B such that 𝗌𝗍𝗋(x) is a prefix of B. Using Lemma 9 (fingerprints) and relying on Lemma 8, we check in O(logm(loglogn)2) time whether the prefix of length mq of the string B is equal to t[q..m) and, thus, we verify whether we indeed performed the descending correctly. We equip T with the same z-fast trie structure.

Searching for 𝒕.

Let s[p..p+|t|)=t be a primary occurrence. Let B be the lowest block in J such that 𝖻(B)p<p+|t|1𝖾(B) and let Bi,,Bj be all children of B in J. Choose the largest h[i..j) such that 𝖾(Bh)<p+|t|1. If B is not a run block, there is q[0..m) such that 𝖻(Bh+1)=p+q and T contains the string t[q..m) and T contains the string t[0..q); the range reporting structure R contains a pair of nodes (x,y) associated with B and index h. If B is a run block, there is q[0..m) such that mq|B1|, 𝖻(B)+c|B1|=p+q, for some c1, and T contains t[q..m) and T contains t[0..q); there is again an associated pair of nodes (x,y). Given the position q, using the fingerprints of prefixes of t[0..q) and t[q..m) in the z-fast tries, one can find in O(log2m(loglogn)2) time the nodes a,b in T and c,d in T on which the range reporting structure R will report all pairs (x,y) that fit the above description and each such reported pair will be associated with a separate primary occurrence (note that a run block B will contain a group of primary occurrences at distances that are multiples of |B1| but the algorithm finds only the rightmost occurrence from each group; other group occurrences are easy to restore using periodicity).

To identify candidate positions q[0..m) for which we perform the described search, we compute the fingerprint f of t, which is a sequence of O(logmloglogn) block identifiers. Let p1,,pb be the starting positions of these blocks from f such that 0=p1<<pb<pb+1=m, i.e., for i[1..b], the ith block from f corresponds to t[pi..pi+1). By Lemma 8, each primary occurrence s[p..p+|t|) of t has the same fingerprint and its respective blocks correspond to the substrings s[p+pi..p+pi+1), for i[1..b]. Let B be the lowest block in J such that 𝖻(B)p<p+|t|1𝖾(B) and let Bi,,Bj be all children of B in the hierarchy of blocks H for s (not in J!). Any substring s[p+pi..p+pi+1), for i[1..b], that corresponds to a non-run block in the fingerprint must form a block in H, which must be a descendant of B in H. Any substring s[p+pi..p+pi+1) that corresponds to a run block with an identifier 𝗂𝖽(Bi),r, for some block Bi and r>1, must form a sequence of blocks s[p+pi+c|Bi|..p+pi+(c+1)|Bi|) in H with c[0..(pi+1pi)/|Bi|), all of which must be descendants of B in H. These observations imply that any block boundary between Bi,,Bj that is inside the range [p..p+|t|) is equal either to p+pi, for some i[1..b], or to p+pi+1|Bi|, where t[pi..pi+1) corresponds to a run block with an identifier Bi,r. We obtain O(logmloglogn) candidate positions for q: all these pi and pi+1|Bi|.

Secondary occurrences.

To find secondary occurrences, we reverse all pointers in the tree J and traverse the reversed pointers starting from each occurrence (including newly added secondary occurrences). More precisely, any non-intermediate leaf block B in J with |B|>1 contains a pointer to another block B^ in J such that 𝗂𝖽(B^)=𝗂𝖽(B); any intermediate block B in J contains two numbers h=𝗈𝖿𝖿(B)0 and r=𝗋(B)>1 and a pointer to a block B^ from a level 2k+2 such that 𝗂𝖽(B) was created from identifiers 𝗂𝖽(Bi+h),,𝗂𝖽(Bi+h+r1), where Bi,,Bj is the sequence of level-(2k+1) blocks from which B^ was created and i+h+r1j. The block B^ has children in J in both cases. Given a leaf B, secondary occurrences of t corresponding to B are exactly all occurrences of t from the substring s[𝖻(B)..𝖾(B)]. They may exist if the block B^ referred by B contains occurrences of t.

We construct on the tree J the marked ancestor data structure [2] that allows us to mark some nodes and to search the nearest marked ancestor of any node in O(1) time. Then, we reverse all pointers: for each block B^, we store in B^ a link to each non-intermediate block B that has a pointer to B^ and we mark B^ in this case; further, if B^ is from a level 2k+2 and B^ was created from level-(2k+1) blocks Bi,,Bj, then we store in B^ a link to each intermediate block B that has a pointer to B^ and we store the range [i+h..i+h+r) corresponding to B in an augmented binary search tree RB^ associated with B^; for any d[i..j], we mark the block Bd if Bd is a non-intermediate child of B^ in J and d[i+h..i+h+r) for a range [i+h..i+h+r) from RB^. Note that the tree RB^ contains at most O((loglogn)2) distinct ranges but one range may correspond to many blocks B; thus, range searching operations on RB^ can be performed in time O(logloglogn)O(logϵn).

With this machinery, the search for secondary occurrences is just a traversal of the reversed pointers. We maintain a set of occurrences of t, which initially contains only primary occurrences, and each occurrence s[p..p+|t|) is associated with a lowest block B^ in J such that 𝖻(B^)p<p+|t|1𝖾(B^). Given such an occurrence s[p..p+|t|) and the associated block B^, we use the tree RB^ to check whether s[p..p+|t|) lies inside some blocks Bi+hBi+h+r1 where [i+h..i+h+r) is a range stored in RB^ and Bi,,Bj are blocks from which B^ was created in the hierarchy of blocks H for s. Each link associated with such range induces a new unique secondary occurrence of t that is added to the set of occurrences. We then loop through all links to all non-intermediate blocks B that have pointers to B^ and each such link analogously induces a new secondary occurrence. Then, we find the nearest marked ancestor B of B (if any) and continue the same process recursively with B in place of B, inducing new unique secondary occurrences. Each added secondary occurrence in the set is processed in the same way. The time for reporting all occ occurrences is O(occlogϵn).

Time.

We obtain u=O(logmloglogn) candidate positions q at which we perform z-fast trie searches and range reporting: O(log2m(loglogn)2+(1+occq)logϵn) time per q, where occq is the number of primary occurrences found for one q. We have occqoccq. Thus, the total time is O(m+u(log2m(loglogn)2+logϵn)+occlogϵn)=O(m+log3m(loglogn)3+logmloglognlogϵn+occlogϵn). Fix a constant ϵ>ϵ. When mlog6n, we estimate logm=O(loglogn) and, thus, the sum log3m(loglogn)3+logmloglognlogϵn is bounded by O(logϵn) and, after renaming ϵ to ϵ, we obtain time O(m+(1+occ)logϵn). When m>log6n, we bound this sum by O(m) and obtain time O(m+occlogϵn).

6 Construction

We build the following components: the compacted tries 𝑻𝗶𝗱, 𝑻, 𝑻, 𝑻𝒇, the range reporting structure 𝑹 and all its pairs (𝒙,𝒚), the trees 𝑨𝑳 and 𝑨𝑹 with a weighted ancestor structure, and, for each block B, pointers to children (if any), the pointer to a copy of B (if B is a leaf), and 𝗯(𝑩), 𝗲(𝑩), (𝑩), 𝗶𝗱(𝑩) (if any). In the end, we also construct in an obvious way all back links for secondary occurrences with a marked ancestor structure [2].

Our algorithm reads s from left to right and maintains for each level of the currently constructed hierarchy H a queue with at most O(loglogn) last built blocks from this level. The letters s[0],s[1], are consecutively fed to the level 0. Each level receives new blocks from the previous level 1 or from s, in case of level 0, and puts them to the right of its queue (the queue elements are ordered from left to right and we dequeue elements from left); when a block is received, we may remove some blocks from the queue and may generate new blocks to feed for level +1. If the level +1 does not exist, we create it. After reading the whole s, we feed into level 0 a special letter $ that forces the algorithm to complete all levels.

At any level , we assume that all level- blocks of the hierarchy H to the left of the leftmost block in the queue were already built together with their descendants and structures like AL, AR, T𝗂𝖽. In particular, we can use Lemma 9 (fingerprints) on these blocks. We maintain a global hash table L that maps any 𝗂𝖽 to the leftmost block in J with this 𝗂𝖽 (as in Lemma 5). By Lemma 5, such leftmost blocks have children in J and are never removed. Thus, once a block with new 𝗂𝖽 is created and the corresponding information about it and its children is added to L, T𝗂𝖽, T, T, Tf, AL, AR, this information will never be removed.

Time.

We use hash tables for L, edge transitions in T𝗂𝖽, T, T, Tf, for van Emde Boas structures from the weighted ancestor structure [25, 36] of AL and AR, and for other places. The query time in the tables is O(1) and the insertion time is amortized O(1) w.h.p. [9] It is the only component that uses randomization. From now on, we do not write explicitly that the time for modifying the tables is “amortized expected w.h.p.”, assuming it by default.

For level =2k or =2k+1, we will spend O(log3|B|logn) time on queue operations for each level- block B with |B|2k, O(log3|B|logn) time on each new block B that we will generate for level +1, and O(1) time on each level- block B with |B|>2k by simply feeding such B to level +1. By Lemma 3, estimating log3|B|=O(k3) when |B|2k, the total time for all levels can be bounded by O(k=0n2kk3logn)=O(nlognk=0k32k)=O(nlogn).

Level =𝟐𝒌, for 𝒌𝟎.

We maintain at most one block B1 in the queue and a number r such that the last r fed blocks had identifiers 𝗂𝖽(B1). Suppose that a new block B2 is fed to us. If 𝗂𝖽(B2)=𝗂𝖽(B1), increment r, done. Let 𝗂𝖽(B2)𝗂𝖽(B1). We remove B1 from the queue and process it as described below (if the queue is not empty), after which we finalize as follows: put B2 in the queue seting r:=1, if |B2|2k, or feed B2 to level +1, if |B2|>2k; done. Now let us describe how B1 and r are processed before the finalization.

If r=1, feed B1 to level +1, finalize. If r>1, create a new block B with 𝗂𝖽(B)=𝗂𝖽(B1),r, 𝖻(B)=𝖻(B1), 𝖾(B)=𝖻(B)+r|B1|1, (B)= (which is correct by Lemma 4) and insert B in AL and AR in O(loglogn) time [36] with a link to the leftmost block with identifier 𝗂𝖽(B1) found using L. If L finds a block B with 𝗂𝖽(B)=𝗂𝖽(B), store in B a pointer to B, feed B to level +1, finalize. If L finds no such B, we proceed as follows.

We insert B into L and set B1 as the only child of B. We can search any prefix of B1 in the z-fast trie T in O(log2|B1|(loglogn)2) time as in the search phase of Section 5, using Lemma 9 (fingerprints) for prefixes of B1; thus, by the binary search on prefixes of B1, we find the location in T at which the string B1 should be inserted in O(log3|B1|(loglogn)2) time. We insert B1 by creating a node x in T such that 𝗌𝗍𝗋(x)=B1 (if it did not exist) and, then, we modify the z-fast trie structure by inserting O(1) new fingerprints into Tf: to insert a fingerprint in Tf, we descend from the root reading it in Tf, skipping edge labels, and then we restore the skipped labels from a reference to an appropriated substring whose fingerprint was stored in Tf, which takes O(log|B1|(loglogn)2) time. Similarly, we insert the string B1r1 into T, creating a node y, in O(log3|B|(loglogn)2) time. Finally, we feed B to level +1 and finalize. We add the pair (x,y) (together with a link to the block B) to the set of pairs for the range reporting structure R, which is built in the very end of the construction in O(NlogN) time, for all N pairs [15]. Since δlognδ=O(n), this time is O(nlogn).

Level =𝟐𝒌+𝟏, for 𝒌𝟎.

Let B1,B2, be all level- blocks from left to right, marked according to conditions (a)–(b) of Section 3: Bh is marked iff |Bh|>2k or |Bh+1|>2k or 𝗂𝖽′′(Bh1) is a local minimum. Suppose that a new block Bj is fed to us. We compute 𝗂𝖽′′(Bj) using the numbers 𝗂𝖽(Bj1), 𝗂𝖽(Bj1). The queue contains the last contiguous sequence of unmarked blocks Bi,,Bj1 fed to level (i.e., Bi1 is marked or i=1). We determine whether Bj1 is marked after Bj is received. As in Section 3, we have ji=O(loglogn).

If |Bj|2k and both Bj1 and Bj are not marked, put Bj in the queue, done. If |Bj|2k and Bj is marked (Bj1 cannot be marked in this case), we process Bi,,Bj and unite them into a new block that is then fed to level +1; we then take Bi,,Bj off the queue, done. If |Bj|>2k, we mark Bj1 and analogously process and unite Bi,,Bj1 instead of Bi,,Bj; then, we move Bj to level +1, done (the queue is empty in the end). Let us describe how Bi,,Bj are processed and united (for Bi,,Bj1, it is analogous).

We read the sequence 𝗂𝖽(Bi),,𝗂𝖽(Bj) in the trie T𝗂𝖽: by descending and skipping edge labels, we reach a node z and restore the skipped labels using a block B referred by z as described in Lemma 6. Then, if we have successfully verified that 𝗌𝗍𝗋(z) equals the sequence, we remove Bi,,Bj and create a new childless block B with (B)=, 𝖻(B)=𝖻(Bi), 𝖾(B)=𝖾(Bj) and we store in B a pointer to B. We insert B in AL and AR with links to leftmost blocks with identifiers 𝗂𝖽(Bi) and 𝗂𝖽(Bj). We then move B to level +1, done.

Suppose that we could not find the above block B in T𝗂𝖽. We create a new block B with a new identifier 𝗂𝖽(B) and we insert the sequence 𝗂𝖽(Bi),,𝗂𝖽(Bj) into T𝗂𝖽 in O(loglogn) time, creating a node z that refers to B (the algorithm is almost the same as reading this sequence in T𝗂𝖽). We insert B in AL and AR with links to leftmost blocks with identifiers 𝗂𝖽(Bi) and 𝗂𝖽(Bj). Then, we must greedily parse Bi,,Bj into subranges: if Bi,,Bm1 have already been parsed (for mi), then Bm,,Bm+r1 is the longest subrange with m+r1j for which there is m<m such that 𝗅𝖾𝖿𝗍(m,4r)=𝗅𝖾𝖿𝗍(m,4r) and 𝗋𝗂𝗀𝗁𝗍(m,5r)=𝗋𝗂𝗀𝗁𝗍(m,5r); let r=1 if there is no such r>0. (We use the notation 𝗅𝖾𝖿𝗍 and 𝗋𝗂𝗀𝗁𝗍 here as in Section 3.) For each subrange Bm,,Bm+r1 with r>1, we form a new intermediate block B~ in its place, removing the blocks Bm,,Bm+r1, and we identify the smallest m′′ such that 𝗋𝗂𝗀𝗁𝗍(m′′,r)=𝗋𝗂𝗀𝗁𝗍(m,r); we store in B~ certain numbers 𝗈𝖿𝖿(B~), 𝗋(B~)=r, and a pointer to the parent of Bm′′ required to find the blocks Bm′′,,Bm′′+r1 (see details in Section 4). After creating all intermediate blocks, the block B has children B~ı~,,B~ȷ~ (intermediate or not) and we do almost the same computations as on level 2k: for each h[ı~..ȷ~), we store the string B~ı~B~h in T, producing a node x, and B~h+1 in T, producing a node y, which also requires a modification of Tf; we add the pair (x,y) to R. All this, except the computation of subranges, takes O(log3|B|(loglogn)3) overall time. We then move B to level +1, done. It remains to describe how Bi,,Bj are parsed into the subranges, which is subtle.

Parsing 𝑩𝒊,,𝑩𝒋.

We maintain two compacted tries T and T defined as follows. Let B be a block of J from a level 2k+2 and Bi,,Bj be the level-(2k+1) blocks from which B=BiBj was created. As Bi,,Bj, the sequence Bi,,Bj was also greedily parsed into subranges: for each such subrange Bm,,Bm+r1 with m[i..j], we store the sequence 𝗂𝖽(Bm),𝗂𝖽(Bm+1),,𝗂𝖽(Bm+r1) in the trie T and T contains an explicit node x such that 𝗌𝗍𝗋(x) equals this sequence, and we store 𝗂𝖽(Bm1),𝗂𝖽(Bm2),,𝗂𝖽(Bi),$ in T with an explicit node y in T with 𝗌𝗍𝗋(y) equal to this sequence. All such pairs (x,y) are stored in a set P. The nodes of T and T store pointers to (leftmost) blocks of J from which their edge labels can be restored. Each node xT (in T, analogously) stores an O(logn)-bit number 𝗏(x) such that, for any x,yT, we have 𝗏(x)<𝗏(y) iff 𝗌𝗍𝗋(x)<𝗌𝗍𝗋(y) (lexicographically); the numbers can be maintained with O(1) amortized insertion time [8, 21] (such structure is called dynamic ordered linked list). Define x<y iff 𝗏(x)<𝗏(y). We store in each node x the smallest and largest nodes, 𝖫(x) and 𝖱(x), in the subtree rooted at x.

We equip P with a dynamic range reporting structure [13, 45]: it uses O(logn) time for insertions and O(logn+tlognloglogn) for queries, where t is the number of reported points. This structure on P needs to know only the relative order of coordinates of its points (x,y), not the values 𝗏(x), 𝗏(y). By Theorem 7, the size of T, T, P is O(δlognδ) (the total number of pairs (x,y)). Once we have parsed Bi,,Bj into subranges, the updates for T, T, P are straightforward (similar to updates for T𝗂𝖽 and R above) and take O((ji+1)logn) time.

Suppose that we have already parsed Bi,,Bm1 into subranges. Let us find the largest r for which there is m<m with 𝗅𝖾𝖿𝗍(m,4r)=𝗅𝖾𝖿𝗍(m,4r) and 𝗋𝗂𝗀𝗁𝗍(m,5r)=𝗋𝗂𝗀𝗁𝗍(m,5r). Initially, set r=1. We consecutively process the positions h=i,,j as follows. We read the sequence 𝗂𝖽(Bh),,𝗂𝖽(Bmin{j,m+5(r+1)1}) by descending from the root of T, thus reaching a node z; set z= if we fail. We read the sequence 𝗂𝖽(Bh1),,𝗂𝖽(Bmax{i1,m4(r+1)}) from T, thus reaching a node v, where we abuse notation by assuming that 𝗂𝖽(Bi1) equals the special symbol $; set v= if we fail. If either hm4(r+1) and z, or hm+5(r+1) and v, then we increment r by 1 and repeat the processing of h again. Otherwise, we continue as follows. If z= or v=, we proceed to the next h; otherwise, we perform the range query in P on [𝖫(z)..𝖱(z)]×[𝖫(v)..𝖱(v)]: if it reports some points, we increment r by 1 and repeat the processing of h again, otherwise, we proceed to the next h.

The reading in T/T takes O(loglogn) time. The queries in P are performed only when h[m4(r+1)..m+5(r+1)). Hence, r was computed in time O((ji)loglogn+rlogn)=O((loglogn)2+rlogn)=O(rlogn). The algorithm is correct by the same reasons why our substring search was correct. Finally, we try to extend r by a naive pattern matching of the sequence 𝗅𝖾𝖿𝗍(m,4(r+1)),𝗋𝗂𝗀𝗁𝗍(m,5(r+1)) in 𝗂𝖽(Bi),,𝗂𝖽(Bj) in O(rloglogn) time. Thus, r is found and it remains to compute the smallest m′′ such that 𝗋𝗂𝗀𝗁𝗍(m′′,r)=𝗋𝗂𝗀𝗁𝗍(m,r).

For each node zT, denote Pz=P[𝖫(z)..𝖱(z)]×[..] and 𝖭(z)=|Pz|. See Fig. 4 in Appendix B. For each z with 𝖭(z)>loglogn, we order Pz according to the y-coordinates of its points and we split Pz into disjoint chunks {Pza}a, each chunk Pza of size Θ(loglogn), so that all y-coordinates in Pza are smaller than in Pza whenever a<a. We store in z a van Emde Boas structure Vz: for each Pza, Vz stores the minimum and maximum of {𝗏(y):(x,y)Pza}. We assign to each point (x,y)P the position px,y such that, at the time of insertion of (x,y) into P, the node x corresponded to a prefix of s[px,y..n) and y to a suffix of s[0..px,y). We store in z a dynamic linear-space RMQ structure Qz from [14] that contains a linked list of all chunks Pza, where each Pza stores the number min{px,y:(x,y)Pza}, and Qz supports insertions/updates in O(lognloglogn) time and, for any b and c, Qz can compute min{px,y: (x,y)a[b..c]Pza} in O(lognloglogn) time. Note that we spent O(1) space per chunk Pza in z.

Figure 4: A schematic depiction of T and T with the points P. The subset PzP corresponding to a node zT is depicted as split into chunks Pza. In each chunk Pza, its points with maximum and minimum y-coordinate are red. Vz stores the values 𝗏(y) for y-coordinates of exactly these red points (O(|Pz|/loglogn) values in total). Qz stores, for each chunk Pza, the minimum of px,y for all (x,y)Pza. The elements of each chunk Pza are not stored explicitly but can be retrieved in O(logn+|Pza|lognloglogn)=O(logn) time using one range reporting on P.

To compute m′′, we process each h(m..m+r) as follows: read the sequence 𝗋𝗂𝗀𝗁𝗍(h,m+rh) in T and 𝗅𝖾𝖿𝗍(h,hm) in T, thus reaching nodes z and v (as in the above analysis). If 𝖭(z)loglogn, we report all t points in the range [𝖫(z)..𝖱(z)]×[𝖫(v)..𝖱(v)] in P in O(logn+tlognloglogn) time, which is O(logn) as t𝖭(z); each point corresponds to an occurrence of the sequence 𝗂𝖽(Bm),,𝗂𝖽(Bm+r1) and we choose the point (x,y) with minimal px,y. If 𝖭(z)>loglogn, we use Vz to find all chunks Pza such that Pza[𝖫(z)..𝖱(z)]×[𝖫(v)..𝖱(v)] and we use Qz to compute minpx,y for all (x,y) from such chunks; two chunks Pza may partially intersect this range and, for each such Pza, we calculate all elements Pza[𝖫(z)..𝖱(z)]×[𝖫(v)..𝖱(v)] using one range reporting query on P. This overall takes time O(logn+|Pza|lognloglogn)=O(logn).

The total space is as follows. For a given node z, all its data structures take O(|Pz|/loglogn) space. Fix a depth in the trie T. The sets Pz for all nodes z with depth partition (a subset of) P and, therefore, in total occupy O(|P|/loglogn) space. Since the maximal depth of T is O(loglogn), the total space is O(|P|).

We maintain all Vz and Qz as follows. Suppose that we insert a point (x,y) in P. We increment 𝖭(z) for all parents z of x. If 𝖭(z)=loglogn, we create Vz and Qz for z with only one chunk. If 𝖭(z)>loglogn, we insert (x,y) into one chunk Pza found using Vz, possibly updating Vz in O(loglogn) time and Qz in O(lognloglogn) (if px,y<min{px~,y~:(x~,y~)Pza}); if Pza overflows, we split it into two chunks with further updates to Qz. To split Pza, we obtain all elements of Pza by one range reporting query on P in O(logn+|Pza|lognloglogn)=O(logn) time. Thus, the amortized time of insertion in Pz is O(lognloglogn) since each chunk Pza, after its creation, can take Θ(loglogn) more insertions before it is split. Hence, the amortized time of insertions in all parents of x in T is O(lognloglogn𝗁𝖾𝗂𝗀𝗁𝗍(T))=O(logn).

When the value 𝗏(v) of vT changes, we should update all structures Vz containing it. To do it efficiently, we store in v as many copies of v as there are points of the form (z,v) in P, each with its own 𝗏(v) and they all are in the ordered linked list with all nodes of T. This does not change the algorithm or space bounds. Let v correspond to a point (z,v)P. In each parent z of z in T, we update Vz, which takes O(loglogn) time (to find whether the old value 𝗏(v) is present in Vz and to update it); O((loglogn)2) time overall. Since the numbers 𝗏() change at most O(n) times in total, the time for all updates is O(n(loglogn)2).

Removed blocks.

Blocks Bi,,Bj on level 2k+1 or B1 on level 2k can be removed. Non-leftmost blocks in J have no children and store pointers to their leftmost copies, which, by Lemma 5, cannot be removed. Hence, no “cleanup” is needed after removing blocks; AL and AR need no changes since their blocks contain links only to undeletable leftmost blocks.

Correctness.

The described algorithm exactly corresponds to the construction of the hierarchy H from Section 3 and it applies all rules 1–4 whenever possible and parses ranges of children blocks into subranges whenever possible. Hence, it indeed produces J.

Theorem 10.

Suppose that s is a string of length n over an alphabet {0,1,,nO(1)} and δ is its string complexity. Using O(δlognδ) machine words, one can construct in one left-to-right pass on s in a streaming fashion an index of size O(δlognδ) that can find in s all occ occurrences of any string of length m in O(m+(occ+1)logϵn) time, where ϵ>0 is any fixed constant (the big-O in space hides factor 1ϵ). The construction time is O(nlogn) w.h.p.

References

  • [1] A. R. Adudodla and D. Kempa. Engineering fast and space-efficient recompression from SLP-compressed text. arXiv preprint arXiv:2506.12011, 2025.
  • [2] S. Alstrup, T. Husfeldt, and T. Rauhe. Marked ancestor problems. In Proc. 39th Annual Symposium on Foundations of Computer Science (Cat. no. 98CB36280), pages 534–543. IEEE, 1998. doi:10.1109/SFCS.1998.743504.
  • [3] A. Andersson and M. Thorup. Dynamic ordered sets with exponential search trees. Journal of the ACM (JACM), 54(3):13–es, 2007. doi:10.1145/1236457.1236460.
  • [4] D. Belazzougui, P. Boldi, R. Pagh, and S. Vigna. Monotone minimal perfect hashing: searching a sorted table with O(1) accesses. In Proc. 20th ACM-SIAM Symposium on Discrete Algorithms (SODA), pages 785–794. SIAM, 2009. doi:10.1137/1.9781611973068.86.
  • [5] D. Belazzougui, P. Boldi, and S. Vigna. Dynamic z-fast tries. In Proc. SPIRE, volume 6393, pages 159–172. Springer, 2010. doi:10.1007/978-3-642-16321-0_15.
  • [6] D. Belazzougui, M. Cáceres, T. Gagie, P. Gawrychowski, J. Kärkkäinen, G. Navarro, A. Ordóñez, S. J. Puglisi, and Y. Tabei. Block trees. Journal of Computer and System Sciences, 117:1–22, 2021. doi:10.1016/j.jcss.2020.11.002.
  • [7] D. Belazzougui, T. Gagie, P. Gawrychowski, J. Kärkkäinen, A. Ordónez, S. J. Puglisi, and Y. Tabei. Queries on LZ-bounded encodings. In Proc. DCC, pages 83–92. IEEE, 2015. doi:10.1109/DCC.2015.69.
  • [8] M. A. Bender, R. Cole, E. D. Demaine, M. Farach-Colton, and J. Zito. Two simplified algorithms for maintaining order in a list. In Algorithms-ESA 2002, volume 2461 of LNCS, pages 152–164. Springer, 2002. doi:10.1007/3-540-45749-6_17.
  • [9] M. A. Bender, M. Farach-Colton, J. Kuszmaul, and W. Kuszmaul. Modern hashing made simple. In Proc. the 2024 Symposium on Simplicity in Algorithms (SOSA), pages 363–373. SIAM, 2024. doi:10.1137/1.9781611977936.33.
  • [10] P. Bille, I. L. Gørtz, M. B. T. Knudsen, M. Lewenstein, and H. W. Vildhøj. Longest common extensions in sublinear space. In Proc. 26th Annual Symposium on Combinatorial Pattern Matching (CPM), volume 9133 of LNCS, pages 65–76. Springer, 2015. doi:10.1007/978-3-319-19929-0_6.
  • [11] P. Bille, I. L. Gørtz, and F. R. Skjoldjensen. Deterministic indexing for packed strings. In Proc. CPM, volume 78 of LIPIcs, pages 6:1–6:11, Dagstuhl, Germany, 2017. Schloss Dagstuhl – Leibniz-Zentrum für Informatik. doi:10.4230/LIPIcs.CPM.2017.6.
  • [12] O. Birenzwige, S. Golan, and E. Porat. Locally consistent parsing for text indexing in small space. In Proc. 14th Annual ACM-SIAM Symposium on Discrete Algorithms (SODA), pages 607–626. SIAM, 2020. doi:10.1137/1.9781611975994.37.
  • [13] G. E. Blelloch. Space-efficient dynamic orthogonal point location, segment intersection, and range reporting. In SODA, volume 8, pages 894–903, 2008.
  • [14] G. S. Brodal, P. Davoodi, and S. S. Rao. Path minima queries in dynamic weighted trees. In Workshop on Algorithms and Data Structures, volume 6844 of LNTCS, pages 290–301. Springer, 2011. doi:10.1007/978-3-642-22300-6_25.
  • [15] T. M. Chan, K. G. Larsen, and M. Pătraşcu. Orthogonal range searching on the RAM, revisited. In Proc. 27th Annual Symposium on Computational Geometry (SoCG), pages 1–10. ACM, 2011. doi:10.1145/1998196.1998198.
  • [16] A. R. Christiansen and M. B. Ettienne. Compressed indexing with signature grammars. In Proc. 13th Latin American Symposium on Theoretical Informatics (LATIN), volume 10807 of LNCS, pages 331–345. Springer, 2018. doi:10.1007/978-3-319-77404-6_25.
  • [17] A. R. Christiansen, M. B. Ettienne, T. Kociumaka, G. Navarro, and N. Prezza. Optimal-time dictionary-compressed indexes. ACM Transactions on Algorithms (TALG), 17(1):1–39, 2020. doi:10.1145/3426473.
  • [18] F. Claude and G. Navarro. Self-indexed grammar-based compression. Fundamenta Informaticae, 111(3):313–337, 2011. doi:10.3233/FI-2011-565.
  • [19] R. Cole and U. Vishkin. Deterministic coin tossing with applications to optimal parallel list ranking. Information and Control, 70(1):32–53, 1986. doi:10.1016/S0019-9958(86)80023-7.
  • [20] K. J. Conrad and P. R. Wilson. Grammatical Ziv-Lempel compression: Achieving PPM-class text compression ratios with LZ-class decompression speed. In Proc. Data Compression Conference (DCC), page 586. IEEE, 2016. doi:10.1109/DCC.2016.119.
  • [21] P. Dietz and D. Sleator. Two algorithms for maintaining order in a list. In Proc. 19th annual ACM Symposium on Theory of Computing (STOC), pages 365–372. ACM, 1987. doi:10.1145/28395.28434.
  • [22] M. Dietzfelbinger, J. Gil, Y. Matias, and N. Pippenger. Polynomial hash functions are reliable. In Proc. 19th International Colloquium on Automata, Languages and Programming (ICALP), volume 623 of LNCS, pages 235–246. Springer, 1992. doi:10.1007/3-540-55719-9_77.
  • [23] T. Gagie, P. Gawrychowski, J. Kärkkäinen, Y. Nekrich, and S. J. Puglisi. LZ77-based self-indexing with faster pattern matching. In Proc. 11th Latin American Symposium on Theoretical Informatics (LATIN), volume 8392 of LNCS, pages 731–742. Springer, 2014. doi:10.1007/978-3-642-54423-1_63.
  • [24] T. Gagie, G. Navarro, and N. Prezza. Fully functional suffix trees and optimal text searching in BWT-runs bounded space. Journal of the ACM (JACM), 67(1):1–54, 2020. doi:10.1145/3375890.
  • [25] P. Gawrychowski, M. Lewenstein, and P.K. Nicholson. Weighted ancestors in suffix trees. In Proc. 22nd Annual European Symposium on Algorithms (ESA), volume 8737 of LNCS, pages 455–466. Springer, 2014. doi:10.1007/978-3-662-44777-2_38.
  • [26] A. Jeż. A really simple approximation of smallest grammar. Theoretical Computer Science, 616:141–150, 2016. doi:10.1016/j.tcs.2015.12.032.
  • [27] R. M. Karp and M. O. Rabin. Efficient randomized pattern-matching algorithms. IBM Journal of research and development, 31(2):249–260, 1987. doi:10.1147/rd.312.0249.
  • [28] D. Kempa and B. Langmead. Fast and space-efficient construction of AVL grammars from the LZ77 parsing. In Proc. 29th Annual European Symposium on Algorithms (ESA), volume 204 of LIPIcs, pages 56:1–56:14. Schloss Dagstuhl – Leibniz-Zentrum für Informatik, 2021. doi:10.4230/LIPIcs.ESA.2021.56.
  • [29] D. Kempa and N. Prezza. At the roots of dictionary compression: string attractors. In Proc. 50th Annual ACM Symposium on Theory of Computing (STOC), pages 827–840. ACM, 2018. doi:10.1145/3188745.3188814.
  • [30] T. Kida, T. Matsumoto, Y. Shibata, M. Takeda, A. Shinohara, and S. Arikawa. Collage system: a unifying framework for compressed pattern matching. Theoretical Computer Science, 298(1):253–272, 2003. doi:10.1016/S0304-3975(02)00426-7.
  • [31] J. C. Kieffer and E.-H. Yang. Grammar-based codes: A new class of universal lossless source codes. IEEE Transactions on Information Theory, 46(3):737–754, 2000. doi:10.1109/18.841160.
  • [32] T. Kociumaka, G. Navarro, and F. Olivares. Near-optimal search time in δ-optimal space, and vice versa. In Proc. 15th Latin American Symposium on Theoretical Informatics (LATIN), volume 13568 of LNCS, pages 88–103. Springer, 2022. doi:10.1007/978-3-031-20624-5.
  • [33] T. Kociumaka, G. Navarro, and F. Olivares. Near-optimal search time in δ-optimal space, and vice versa. Algorithmica, 86(4):1031–1056, 2024. doi:10.1007/s00453-023-01186-0.
  • [34] T. Kociumaka, G. Navarro, and N. Prezza. Towards a definitive measure of repetitiveness. In Proc. 14th Latin American Symposium on Theoretical Informatics (LATIN), volume 12118 of LNCS, pages 207–219. Springer, 2020. doi:10.1007/978-3-030-61792-9_17.
  • [35] T. Kociumaka, G. Navarro, and N. Prezza. Toward a definitive compressibility measure for repetitive sequences. IEEE Transactions on Information Theory, 69(4):2074–2092, 2022. doi:10.1109/TIT.2022.3224382.
  • [36] T. Kopelowitz and M. Lewenstein. Dynamic weighted ancestors. In Proc. 18th Annual ACM-SIAM Symposium on Discrete Algorithms (SODA), pages 565–574. ACM-SIAM, 2007. doi:10.5555/1283383.1283444.
  • [37] D. Köppl, F. Kurpicz, and D. Meyer. Faster block tree construction. In Proc. 31st Annual European Symposium on Algorithms (ESA), volume 274 of LIPIcs, pages 74:1–74:20, Dagstuhl, Germany, 2023. Schloss Dagstuhl – Leibniz-Zentrum für Informatik. doi:10.4230/LIPIcs.ESA.2023.74.
  • [38] D. Kosolobov. Compressed index with construction in compressed space. arXiv preprint, 2026. arXiv:2602.13735.
  • [39] D. Kosolobov and N. Sivukhin. Construction of sparse suffix trees and LCE indexes in optimal time and space. In Proc. 35th Annual Symposium on Combinatorial Pattern Matching (CPM), volume 296 of LIPIcs, pages 20:1–20:18. Schloss Dagstuhl – Leibniz-Zentrum für Informatik, 2024. doi:10.4230/LIPIcs.CPM.2024.20.
  • [40] K. Mehlhorn, R. Sundar, and C. Uhrig. Maintaining dynamic sequences under equality tests in polylogarithmic time. Algorithmica, 17(2):183–198, 1997. doi:10.1007/BF02522825.
  • [41] J. Ian Munro, Gonzalo Navarro, and Yakov Nekrich. Text indexing and searching in sublinear time. In Proc. 31st Annual Symposium on Combinatorial Pattern Matching (CPM), volume 161 of LIPIcs, pages 24:1–24:15, Dagstuhl, Germany, 2020. Schloss Dagstuhl – Leibniz-Zentrum für Informatik. doi:10.4230/LIPIcs.CPM.2020.24.
  • [42] G. Navarro. A self-index on block trees. In Proc. 24th International Symposium on String Processing and Information Retrieval (SPIRE), volume 10508 of LNCS, pages 278–289. Springer, 2017. doi:10.1007/978-3-319-67428-5_24.
  • [43] G. Navarro. Indexing highly repetitive string collections, part i: Repetitiveness measures. ACM Computing Surveys (CSUR), 54(2):1–31, 2021. doi:10.1145/3434399.
  • [44] Gonzalo Navarro and Nicola Prezza. Universal compressed text indexing. Theoretical Computer Science, 762:41–50, 2019. doi:10.1016/j.tcs.2018.09.007.
  • [45] Y. Nekrich. Orthogonal range searching in linear and almost-linear space. Computational Geometry, 42(4):342–351, 2009. doi:10.1016/j.comgeo.2008.09.001.
  • [46] T. Nishimoto, T. I, S. Inenaga, H. Bannai, and M. Takeda. Dynamic index and LZ factorization in compressed space. Discrete Applied Mathematics, 274:116–129, 2020. doi:10.1016/j.dam.2019.01.014.
  • [47] S. Raskhodnikova, D. Ron, R. Rubinfeld, and A. Smith. Sublinear algorithms for approximating string compressibility. Algorithmica, 65(3):685–709, 2013. doi:10.1007/s00453-012-9618-6.
  • [48] M. Ružić. Constructing efficient dictionaries in close to sorting time. In Proc. International Colloquium on Automata, Languages, and Programming (ICALP), volume 5125 of LNTCS, pages 84–95. Springer, 2008. doi:10.1007/978-3-540-70575-8_8.
  • [49] S. C. Sahinalp and U. Vishkin. Symmetry breaking for suffix tree construction. In Proc. 26th Annual ACM Symposium on Theory of Computing (STOC), pages 300–309. ACM, 1994. doi:10.1145/195058.195164.
  • [50] Y. Takabatake, T. I, and H. Sakamoto. A space-optimal grammar compression. In Proc. 25th Annual European Symposium on Algorithms (ESA), volume 87 of LIPIcs, pages 67:1–67:15, Dagstuhl, Germany, 2017. Schloss Dagstuhl – Leibniz-Zentrum für Informatik. doi:10.4230/LIPIcs.ESA.2017.67.
  • [51] F. Zhang, W. Wan, C. Zhang, J. Zhai, Y. Chai, H. Li, and X. Du. CompressDB: Enabling efficient compressed data direct processing for various databases. In Proc. 2022 International Conference on Management of Data, pages 1655–1669, 2022. doi:10.1145/3514221.3526130.
  • [52] J. Ziv and A. Lempel. A universal algorithm for sequential data compression. IEEE Transactions on Information Theory, 23(3):337–343, 1977. doi:10.1109/TIT.1977.1055714.

Appendix A Hierarchy of Blocks (Missing Proofs)

Lemma 2 (local consistency). [Restated, see original statement.]

Fix =2k or =2k+1. Let B1,B2,,Bb denote all level- blocks. For any i, j and block Bh, if s[i2k+4..j+2k+4]=s[𝖻(Bh)2k+4..𝖾(Bh)+2k+4], then there exists a level- block Bh such that 𝗂𝖽(Bh)=𝗂𝖽(Bh), 𝖻(Bh)=i, and 𝖾(Bh)=j.

Proof.

The proof is by induction on . For =0, it is trivial. Assume >0. Let =1. Denote ı~=𝖻(Bh) and ȷ~=𝖾(Bh). Suppose that Bh=B^uB^v, where B^u,,B^v are level- blocks corresponding to Bh. By the inductive hypothesis, there are level- blocks B^u,,B^v such that vu=vu and these blocks correspond to the substring s[i..j] in the sense that i=𝖻(B^u) and 𝗂𝖽(B^u)=𝗂𝖽(B^u),,𝗂𝖽(B^v)=𝗂𝖽(B^v). Note that, for the inductive hypothesis to hold, it was sufficient to have a weaker assumption that s[i2k+3..j+2k+3]=s[ı~2k+3..ȷ~+2k+3].

Since the construction process copies blocks of length greater than 2k from level to level unaltered, all blocks B^u,,B^v either have length at most 2k, or u=v and |B^u|>2k. In the latter case, we have u=v, 𝗂𝖽(B^u)=𝗂𝖽(B^u)=𝗂𝖽(Bh) and the block B^u will be copied to level in the same way as Bu had been copied to level , which implies that the required level- block Bh is the copy of B^u.

From now on, assume that each of the blocks B^u,,B^v has length at most 2k. Observe that, since 0<ij<|s|1 and 0<ı~ȷ~<|s|1, neither of the blocks B^u,B^v,B^u,B^v is the first or last block on level .

Let =2k+1. We will prove the inductive step under the following weaker assumption: s[i2k+32k..j+2k+3+2k]=s[ı~2k+32k..ȷ~+2k+3+2k] (it will be useful in the analysis of the case =2k+2). As it was noted above, such weaker assumption still implies the existence of the blocks B^u,,B^v corresponding to the substring s[i..j]. We have 𝗂𝖽(B^u)==𝗂𝖽(B^v) and 𝗂𝖽(B^u1)𝗂𝖽(B^u) and 𝗂𝖽(B^v)𝗂𝖽(B^v+1). Since 𝗂𝖽(B^u)=𝗂𝖽(B^u),,𝗂𝖽(B^v)=𝗂𝖽(B^v), it remains to prove that 𝗂𝖽(B^u1)𝗂𝖽(B^u) and 𝗂𝖽(B^v)𝗂𝖽(B^v+1). If |B^u1|2k, then the substring B^u1=s[ı~|B^u1|..ı~) is inside s[ı~2k+32k..ȷ~+2k+3+2k] together with a “neighborhood” of length 2k+3 since |B^u1|+2k+32k+2k+3. Therefore, by the inductive hypothesis, the substring s[i|B^u1|..i), which is equal to B^u1, must form a level- block B^u1 with 𝗂𝖽(B^u1)=𝗂𝖽(B^u1). Hence, we obtain 𝗂𝖽(Bu1)𝗂𝖽(B^u) since 𝗂𝖽(B^u1)𝗂𝖽(B^u). By the same reasoning, if |B^u1|>2k, then |B^u1|>2k and vice versa (due to symmetry). In case |B^u1|>2k, we have 𝗂𝖽(B^u1)𝗂𝖽(B^u) since |B^u|2k. Symmetrically, one can show that 𝗂𝖽(B^v)𝗂𝖽(B^v+1).

Let =2k+2. To generate level- blocks (including Bh), the process marked some blocks from level 2k+1 using rules (a) and (b) described in Section 3. According to these rules, B^u1 and B^v must be marked and B^u,,B^v1 must all be unmarked. It suffices to prove that B^u1 and B^v are marked and B^u,,B^v1 are unmarked. Let us first derive a certain technical observation.

Suppose that, for some t[1 .. 7], each of the blocks B^ut,,B^u1 has length at most 2k. Since t2k+2k+3+2k2k+4, each of these blocks is inside the substring s[ı~2k+4..ȷ~+2k+4] together with a “neighborhood” of length 2k+3+2k. As was proved in case =2k+1, it follows that the substrings s[i|B^utB^u1|..i|B^ut+1B^u1|),,s[i|B^u1|..i), which are equal to B^ut,,B^u1, respectively, must form level- blocks B^ut,,B^u1 such that 𝗂𝖽(B^ut)=𝗂𝖽(B^ut),,𝗂𝖽(B^u1)=𝗂𝖽(B^u1). Symmetrically, if each of the blocks B^ut,,B^u1 has length at most 2k, then also 𝗂𝖽(B^ut)=𝗂𝖽(B^ut),,𝗂𝖽(B^u1)=𝗂𝖽(B^u1).

Suppose that B^u1 was marked because of rule (b), i.e., >𝗂𝖽′′(B^u3)>𝗂𝖽′′(B^u2) and 𝗂𝖽′′(B^u2)<𝗂𝖽′′(B^u1)<. Hence, each of the five blocks B^u5,,B^u1 has length at most 2k. Due to the above observation, we have 𝗂𝖽(B^u5)=𝗂𝖽(B^u5),,𝗂𝖽(B^u1)=𝗂𝖽(B^u1). Therefore, the sequence 𝗂𝖽′′(B^u3),,𝗂𝖽′′(B^v) is equal to the sequence 𝗂𝖽′′(B^u3),,𝗂𝖽′′(B^v). Hence, B^u1 must be marked and B^u,,B^v1 must be unmarked. We can analogously prove that, if B^v was marked due to rule (b), then B^v is also marked.

Suppose that B^u1 was marked because of rule (a), i.e., |B^u1|>2k or |B^u|>2k. The latter is impossible since we assumed that |B^u|2k. Now, again due to the above observation, we have |B^u1|>2k. Thus, 𝗂𝖽′′(B^u1)= and, hence, B^u1 is marked. Further, the sequences 𝗂𝖽′′(B^u1),,𝗂𝖽′′(B^v) and 𝗂𝖽′′(B^u1),,𝗂𝖽′′(B^v) coincide, which implies that B^u,,B^v1 are unmarked and, again, if B^v was marked due to rule (b), then B^v is marked.

It remains to consider the case when B^v is marked due to rule (b) and to prove that B^v must be marked too in this case. We assumed |B^v|2k and, hence, the only option is that |B^v+1|>2k. But we can repeat the proof of the above observation to deduce that |B^v+1|2k iff |B^v+1|2k. Therefore, we obtain |B^v+1|>2k and, thus, B^v is marked.

Lemma 4. [Restated, see original statement.]

Any blocks B and B with 𝗂𝖽(B)=𝗂𝖽(B) are equal as strings and both were created on the same level.

Proof.

The equality as strings is true by construction. Suppose, to the contrary, that the blocks B and B were created on levels and , respectively, with < and is the smallest such level where the lemma fails. Let =2k+2, for some k, and B was created from blocks Bi,,Bj from level 2k+1 so that B=BiBj. Since 𝗂𝖽(B)=𝗂𝖽(B), there are blocks Bi,,Bj on a level < from which the block B was created such that ji=ji and 𝗂𝖽(Bi)=𝗂𝖽(Bi),,𝗂𝖽(Bj)=𝗂𝖽(Bj). Since 2k+1< and is the minimal level where the lemma fails, BiBj were present on level 2k+1. The length of each of the blocks Bi,,Bj is at most 2k and, then, the same holds for Bi,,Bj. Thus, the latter blocks should have participated in a maximal range of blocks with lengths 2k. In such a range, all blocks are united into disjoint subranges, each of which contains at least two blocks, except, possibly, the last subrange, which can be formed by one block. Each subrange generates a block for level 2k+2. But this contradicts the assumption that the blocks Bi,,Bj should have been transitioned unaffected to level 1=2k+2.

Let =2k+1 for some k. One can analogously find decompositions of B and B into blocks from level 2k: B=BiBj and B=BiBj, where ji=ji and 𝗂𝖽(Bi)=𝗂𝖽(Bi),,𝗂𝖽(Bj)=𝗂𝖽(Bj). Since =2k+1, we have 𝗂𝖽(Bi)==𝗂𝖽(Bj) and B was produced by uniting these blocks into a “run”. Then, Bi,,Bj should have been similarly united on the level 1, a contradiction.

Appendix B Jiggly Block Tree (Missing Proofs)

Lemma 5. [Restated, see original statement.]

For d0, let B be the leftmost topmost block with 𝗂𝖽(B)=d and |B|>1 in the hierarchy H, i.e., 𝖻(B) is minimal for B among all blocks with this 𝗂𝖽 and B is such block with maximal level. The jiggly block tree J retains the block B and B has children in J.

Proof.

Consider the process that constructs J by pruning the original hierarchy tree H applying, first, rules 1–4 and, then, introducing intermediate blocks. Suppose, to the contrary, that a certain rule either removed B itself or all children of B and assume that B is the first leftmost and topmost block with any 𝗂𝖽 for which this happened. Let denote the level of B.

The rules 1–2 obviously could not do this (recall that |B|>1). Rule 3 cannot be applied to B itself since B has no “preceding” blocks with equal 𝗂𝖽. Rule 4 applied to B itself does not remove all children and, thus, is not contradictory.

One can show by induction on levels that, for any blocks B and B′′, if 𝗂𝖽(B)=𝗂𝖽(B′′), then, given =2k or =2k+1, any level- block from the subtree rooted at B in the tree H has a corresponding block with equal identifier in the subtree rooted at B′′. It is then straightforward that a rule 3 or 4 applied to a block cannot remove a block B containing B as a descendant since, for such a rule, there is always an unaffected block B′′ “preceding” B with 𝗂𝖽(B′′)=𝗂𝖽(B) and, hence, B should have been among the descendants of B′′ if B were a descendant of B, which is impossible because B is already leftmost.

Suppose that B was pruned due to the introduction of an intermediate block B in place of a subrange of blocks BmBm+r1 with r>1 from a level 2k+1. By construction, there is m<m such that 𝗂𝖽(Bm+h)=𝗂𝖽(Bm+h), for all h[0..r). If B were a descendant of Bm+h, for h[0..r) (maybe Bm+h=B), then B must be a descendant of Bm+h in the tree H by the observation discussed above. This is a contradiction since B is leftmost.

Theorem 7. [Restated, see original statement.]

The jiggly block tree for string s of length n has size O(δlognδ), where δ is the string complexity of s, provided δΩ(loglogn).

Proof (continuation)..

Let us detail the ideas described in the conceptual part of the proof from the main text.

Part i.

Fix =2k+1 or =2k+2, for some k. Consider a level- block B in J that is not a leaf. By Lemma 3 (local sparsity), there are O(1) such blocks B for which 𝖻(B)<2k+6 or 𝖾(B)+2k+6n. Assume that these conditions do not hold for B and, hence, the substring s[𝖻(B)2k+6..𝖾(B)+2k+6] is well defined. We have |B|2k since otherwise B should have had a copy on level +1 that is the parent of B in the original tree H and, hence, B could be removed by rule 2. Let us show that the string s[𝖻(B)2k+4..𝖾(B)+2k+4] has no occurrences at positions smaller than 𝖻(B)2k+4. Suppose, to the contrary, that it occurs at a position i2k+4 with i<𝖻(B). Then, due to Lemma 2 (local consistency), there is a block B=s[i..i+|B|) in the tree H such that 𝗂𝖽(B)=𝗂𝖽(B). By Lemma 5, the tree J contains the leftmost and topmost such block B′′ with 𝗂𝖽(B′′)=𝗂𝖽(B) and 𝖻(B′′)<𝖻(B). But then all descendants of B could be removed by rule 3, which is a contradiction.

The length of s[𝖻(B)2k+4..𝖾(B)+2k+4] is 22k+4+|B|2k+5+2k. Hence, the strings s[h..h+2k+6) with h(𝖻(B)2k+42k..𝖻(B)2k+4] all cover the substring s[𝖻(B)2k+4..𝖾(B)+2k+4] and, thus, must be distinct and each such string s[h..h+2k+6) has no occurrences at positions smaller than h. We assign the block B to each of these 2k strings s[h..h+2k+6). We do the same analysis and assignments for every internal level- block in J. Due to Lemma 3 (local sparsity), we could have assign at most O(1) blocks to any fixed substring s[h..h+2k+6) with h[0..n2k+6). Denote by d2k+6 the number of distinct substrings of length 2k+6 and by b the number of internal blocks on level in J. We associate each distinct substring of length 2k+6 with its leftmost occurrence s[h..h+2k+6). Thus, every internal block on level is assigned to exactly 2k distinct substrings. We obtain b2kO(d2k+6). Hence, bO(d2k+6/2k)O(δ). So, the number of internal level- blocks in J is O(δ) and the total number of internal blocks in J is O(δlognδ). Let us count leaves.

Part ii.

First, let us restrict our analysis to only certain leaf blocks B. Denote by 𝗉𝖺𝗋(B) the parent of B. Since the total number of internal blocks is O(δlognδ), the number of leaf blocks B such that either 2|B||𝗉𝖺𝗋(B)| or B is the first or last child of 𝗉𝖺𝗋(B) is at most O(δlognδ) (across all levels). Therefore, it suffices to consider the case when 2|B|<|𝗉𝖺𝗋(B)| and B is neither first nor last child of 𝗉𝖺𝗋(B). Further, there are at most O(logn) blocks B such that 𝖻(B)211|B| and 𝖾(B)+211|B|n and each such block B contributes at most O(loglogn) leaf-children, i.e., O(lognloglogn) in total, which is O(δlognδ) since it was assumed that δΩ(loglogn). Thus, we can assume that B=𝗉𝖺𝗋(B), the parent of the leaf block B, is “far enough” from both ends of the string s so that the substring s[𝖻(B)211|B|..𝖾(B)+211|B|] is well defined. The block B=𝗉𝖺𝗋(B) is not a run block, due to rule 4; hence, B is from a level 2k+1, for some k (B might be intermediate).

() Restriction: it suffices to count only leaf blocks B from levels 2k+1 with k0 such that, for B=𝗉𝖺𝗋(B), we have 2|B|<|B| and B is neither first nor last child of B, and 𝖻(B)>211|B| and 𝖾(B)+211|B|<n.

Let B be a leaf block from level 2k+1 as in the restriction and B=𝗉𝖺𝗋(B). Denote by B1,,Bb all blocks from level 2k+1 in the hierarchy H; we mark them according to conditions (a) and (b) from Section 3 so that we can use the functions 𝗅𝖾𝖿𝗍 and 𝗋𝗂𝗀𝗁𝗍 from now on. Let Bi,,Bj be a range of blocks from which B=BiBj was produced in H. Thus, B=BmBm+r1 for some m and r such that [m..m+r)(i..j). We have m+rj since B is not the last child of B. The block B is either intermediate (if r>1) or an original block from H (if r=1). Denote x=𝖻(B) and y=𝖾(B)+|Bm+r| and consider the substring s[x..y]=BmBm+r. The subrange Bm,,Bm+r1 was distinguished by the greedy parsing procedure described in Section 4, according to which there are no indices m<m such that 𝗅𝖾𝖿𝗍(m,4(r+1))=𝗅𝖾𝖿𝗍(m,4(r+1)) and 𝗋𝗂𝗀𝗁𝗍(m,5(r+1))=𝗋𝗂𝗀𝗁𝗍(m,5(r+1)). Note that the length of each of the blocks Bi,,Bj is at most 2k. As s[x..y]=BmBm+r, one can show that the string s[xz..y+z] with z=4(r+2)2k+2k+4 cannot occur at any position xz with x<x since, otherwise, Lemma 2 (local consistency) would imply that there is m<m such that 𝗅𝖾𝖿𝗍(m,4(r+1))=𝗅𝖾𝖿𝗍(m,4(r+1)) and 𝗋𝗂𝗀𝗁𝗍(m,5(r+1))=𝗋𝗂𝗀𝗁𝗍(m,5(r+1)). Any block Bh with h[1..b] is marked iff |Bh|>2k or |Bh+1|>2k or Bh is preceded by a local minimum 𝗂𝖽′′(Bh1); to calculate the local minimum, we need the blocks Bh4,Bh3,Bh2,Bh1. If 𝗅𝖾𝖿𝗍(m,4(r+1)) does not contain the special symbol $, then z is large enough to guarantee that at any occurrence s[xz..y+z] of s[xz..y+z] there are exactly the same 4(r+1) blocks Bm4(r+1),,Bm1, each of length 2k, to the left of s[x..y] and, moreover, neither of them could be marked due to a local minimum since, if so, then the four blocks Bm4(r+1)4,,Bm4(r+1)1, because of which the local minimum appeared, should have occured at both positions by Lemma 2 (as z is large enough). The case when 𝗅𝖾𝖿𝗍 contains $ is analogous and the analysis of 𝗋𝗂𝗀𝗁𝗍 is also analogous.

Denote by k the smallest integer such that kk+4 and 2k>|BmBm+r|. It follows from Lemma 3 (local sparsity) that r+2262k/2k and, hence, 2k+6(r+2)2k and 2k+8+2k4(r+2)2k+2k+4=z. Therefore, 2k+10>22k+8+22k2z+|BmBm+r|+2k and the strings s[h..h+2k+10) with h(xz2k..xz] all cover s[xz..y+z] and, thus, must be distinct and each such string s[h..h+2k+10) has no occurrences at positions smaller than h (note that all these strings are well defined since the block B is “far enough” from both ends of the string s). We assign the block B to each of these 2k strings s[h..h+2k+10) with h(xz2k..xz]. We do the same analysis and assignments for every leaf block B in J that satisfies the conditions from restriction ().

Fix a substring s[h..h+2k+10) with h[0..n2k+10) and k>0. Let us show that O(1) blocks were assigned to this substring in total. Let k be such that k=k+4. Then, by Lemma 3 (local sparsity), at most O(1) blocks were assigned from levels 2k. Let k be such that k>k+4. If a block B=BmBm+r1 from level 2k+1 was assigned to the string s[h..h+2k+10), where Bm,,Bm+r are blocks from level 2k+1 corresponding to B as in the description above, then we have 2k>|BmBm+r|2k1. We associate the block B with the interval I(B)=[𝖻(Bm)..𝖾(Bm+r)] of length between 2k1 and 2k that is a subset of [h..h+2k+10). For each block B assigned to s[h..h+2k+10) from every level 2k+1 such that k>k+4, we associate an interval I(B) in this was. If intervals I(B) and I(B) intersect, then either they are nested (and the blocks B and B are nested) or B and B are from the same level. A given interval I(B) may intersect at most two other intervals associated with blocks from the same level. If a block B was assigned to the same string as block B and B is an ancestor of B in J (they are nested), then for any ancestor B′′ of B, |B′′|>2|B|2|BmBm+r|2k and, hence, B′′ could not be assigned to the same string s[h..h+2k+10). So, the system of intervals I(B) associated with all blocks B assigned to the substring s[h..h+2k+10) contains O(2k+10/2k)=O(1) intervals: to see this, count separately the intervals that are not nested in any intervals, and all remaining intervals.

Fix k. Denote by d2k+10 the number of distinct substrings of length 2k+10 and by k the set of blocks that were assigned to all strings s[h..h+2k+10) with h[0..n2k+10). We associate each distinct substring of length 2k+10 with its leftmost occurrence s[h..h+2k+10). Thus, every block from k was assigned to exactly 2k such distinct substrings. We obtain |k|2kO(d2k+10) and, hence, |k|O(d2k+10/2k)O(δ). Therefore, one can estimate as O(δlognδ) the number of leaf blocks that were assigned to substrings of length 2k+10 with klognδ as described above, i.e., k=1log(n/δ)|k|O(δlognδ).

Part iii.

It remains to count the leaf blocks that were assigned to substrings of length 2k+10 with k>lognδ. Fix such k. Consider a leaf block B from a level 2k+1 that was assigned to a substring of length 2k+10. Let B=BmBm+r1, where Bm,,Bm+r are blocks from level 2k+1 corresponding to B as in the description above. We associate B with the interval I(B)=[𝖻(Bm)..𝖾(Bm+r)]. We do the same association to intervals for all blocks B assigned to substrings of length 2k+10. As in the above discussion, one can show that all such intervals I(B) are distinct, there is no “double nestedness” such as I(B)I(B)I(B′′), and an interval I(B) can intersect at most two other intervals I(B) and I(B′′) that are not nested in it and do not contain it and B and B′′ must be from the same level as B in this case. Since, for each such B, |I(B)|2k1, one can estimate the number of intervals as O(n/2k) by counting separately the intervals that are not nested inside other intervals, and all remaining intervals. Therefore, the total number of blocks assigned to substrings of length 2k+10 for all k>lognδ is O(kn/2k)=O(n/2log(n/δ))=O(δ).