Finger Search in Grammar-Compressed Strings

Grammar-based compression, where one replaces a long string by a small context-free grammar that generates the string, is a simple and powerful paradigm that captures many popular compression schemes. Given a grammar, the random access problem is to compactly represent the grammar while supporting random access, that is, given a position in the original uncompressed string report the character at that position. In this paper we study the random access problem with the finger search property, that is, the time for a random access query should depend on the distance between a specified index $f$, called the \emph{finger}, and the query index $i$. We consider both a static variant, where we first place a finger and subsequently access indices near the finger efficiently, and a dynamic variant where also moving the finger such that the time depends on the distance moved is supported. Let $n$ be the size the grammar, and let $N$ be the size of the string. For the static variant we give a linear space representation that supports placing the finger in $O(\log N)$ time and subsequently accessing in $O(\log D)$ time, where $D$ is the distance between the finger and the accessed index. For the dynamic variant we give a linear space representation that supports placing the finger in $O(\log N)$ time and accessing and moving the finger in $O(\log D + \log \log N)$ time. Compared to the best linear space solution to random access, we improve a $O(\log N)$ query bound to $O(\log D)$ for the static variant and to $O(\log D + \log \log N)$ for the dynamic variant, while maintaining linear space. As an application of our results we obtain an improved solution to the longest common extension problem in grammar compressed strings. To obtain our results, we introduce several new techniques of independent interest, including a novel van Emde Boas style decomposition of grammars.


Introduction
Grammar-based compression, where one replaces a long string by a small context-free grammar that generates the string, is a simple and powerful paradigm that captures many popular compression schemes including the Lempel-Ziv family [49,48,46], Sequitur [35], Run-Length Encoding, Re-Pair [32], and many more [40,20,29,30,47,4,2,3,26]. All of these are or can be transformed into equivalent grammar-based compression schemes with little expansion [38,14].
Given a grammar S representing a string S, the random access problem is to compactly represent S while supporting fast access queries, that is, given an index i in S to report S [i]. The random access problem is one of the most basic primitives for computation on grammar compressed strings, and solutions to the problem are a key component in a wide range of algorithms and data structures for grammar compressed strings [9,10,21,22,23,8,28,42,43,6].
In this paper we study the random access problem with the finger search property, that is, the time for a random access query should depend on the distance between a specified index f , called the finger, and the query index i. We consider two variants of the problem. The first variant is static finger search, where we can place a finger with a setfinger operation and subsequently access positions near the finger efficiently. The finger can only be moved by a new setfinger operation, and the time for setfinger is independent of the distance to the previous position of the finger. The second variant is dynamic finger search, where we also support a movefinger operation that updates the finger such that the update time depends on the distance the finger is moved.
Our main result is efficient solutions to both finger search problems. To state the bounds, let n be the size the grammar S, and let N be the size of the string S. These are the first non-trivial bounds for the finger search problems.
As an application of our results we also give a new solution to the longest common extension problem on grammar compressed strings [9,28,36]. Here, the goal is to compactly represent S while supporting fast lce queries, that is, given a pair of indices i, j to compute the length of the longest common prefix of S[i, N ] and S[j, N ]. We give an O(n) space representation that answers queries in O(log N + log 2 ), where is the length of the longest common prefix. The best O(n) space solution for this problem uses O(log N log ) time, and hence our new bound is always at least as good and better whenever = o(N ε ).

Related Work
We briefly review the related work on the random access problem and finger search.

Random Access in Grammar Compressed Strings.
First note that naively we can store S explicitly using O(N ) space and report any character in constant time. Alternatively, we can compute and store the sizes of the strings derived by each grammar symbol in S and use this to simulate a top-down search on the grammars derivation tree in constant time per node. This leads to an O(n) space representation using O(h) time, where h is the height of the grammar [25]. Improved succinct space representation of this solution are also known [15]. Bille et al. [10] gave a solution using O(n) and O(log N ) time, thus achieving a query time independent of the height of the grammar. Verbin and Yu [45] gave a near matching lower bound by showing that any solution using O(n log O(1) N ) space must use Ω(log 1− N ) time. Hence, we cannot hope to obtain significantly faster query times within O(n) space. Finally, Belazzougui et al. [6] very recently showed that with superlinear space slightly faster query times are possible. Specifically, they gave a solution using O(nτ log τ N/n) space and 36:3 O(log τ N ) time, where τ is a trade-off parameter. For τ = log N this is O(n log N ) space and O(log N/ log log N ) time. Practical solutions to this problem have been considered in [5,34,24].
The above solutions all generalize to support decompression of an arbitrary substring of length D in time O(t access + D), where t access is the time for access (and even faster for small alphabets [6]). We can extend this to a simple solution to finger search (static and dynamic). The key idea is to implement setfinger as a random access and access and movefinger by decompressing or traversing, respectively, the part of the grammar in-between the two positions. This leads to a solution that uses O(t access ) time for setfinger and O(D) time for access and movefinger.
Another closely related problem is the bookmarking problem, where a set of positions, called bookmarks, are given at preprocessing time and the goal is to support fast substring decompression from any bookmark in constant or near-constant time per decompressed character [16,21]. In other words, bookmarking allows us to decompress a substring of length D in time O(D) if the substring crosses a bookmark. Hence, with bookmarking we can improve the O(t access + D) time solution for substring decompression to O(D) whenever we know the positions of the substrings we want to decompress at preprocessing time. A key component in the current solutions to bookmarking is to trade-off the Ω(D) time we need to pay to decompress and output the substring. Our goal is to support access without decompressing in o(D) time and hence this idea does not immediately apply to finger search.
Finger Search. Finger search is a classic and well-studied concept in data structures, see e.g., [7,11,13,39,17,19,27,33,31,37,41] and the survey [12]. In this setting, the goal is to maintain a dynamic dictionary data structure such that searches have the finger search property. Classic textbook examples of efficient finger search dictionaries include splay trees, skip lists, and level linked trees. Given a comparison based dictionary with n elements, we can support optimal searching in O(log n) time and finger searching in O(log d) time, where d is the rank distance between the finger and the query [12]. Note the similarity to our compressed results that reduce an O(log N ) bound to O(log D).

Our results
We now formally state our results. Let S be a string of length N compressed into a grammar S of length n. Our goal is to support the following operations on S. The static finger problem is to support access and setfinger, and the dynamic finger search problem is to support all three operations. We obtain the following bounds for the finger search problems.

36:4 Finger Search in Grammar-Compressed Strings
Compared to the previous best linear space solution, we improve the O(log N ) bound to O(log D) for the static variant and to O(log D + log log N ) for the dynamic variant, while maintaining linear space. These are the first non-trivial solutions to the finger search problems. Moreover, the logarithmic bound in terms of D may be viewed as a natural grammar compressed analogue of the classic uncompressed finger search solutions. We note that Theorem 1 is straightforward to generalize to multiple fingers. Each additional finger can be set in O(log N ) time, uses O(log N ) additional space, and given any finger f , we can support

Technical Overview
To obtain Theorem 1 we introduce several new techniques of independent interest. First, we consider a variant of the random access problem, which we call the fringe access problem.
Here, the goal is to support fast access close to the beginning or end (the fringe) of a substring derived by a grammar symbol. We present an O(n) space representation that supports fringe access from any grammar symbol v in time O(log D v + log log N ), where D v is the distance from the fringe in the string S(v) derived by v to the queried position.
The main component in our solution to this problem is a new recursive decomposition. The decomposition resembles the classic van Emde Boas data structure [44], in the sense that we recursively partition the grammar into a hierarchy of depth O(log log N ) consisting of subgrammars generating strings of lengths N 1/2 , N 1/4 , N 1/8 , . . .. We then show how to implement fringe access via predecessor queries on special paths produced by the decomposition. We cannot afford to explicitly store a predecessor data structure for each special path, however, using a technique due to Bille et al. [10], we can represent all the special paths compactly in a tree and instead implement the predecessor queries as weighted ancestor queries on the tree. This leads to an O(n) space solution with O(log D v + (log log N ) 2 ) query time. Whenever D v ≥ 2 (log log N ) 2 this matches our desired bound of O(log D v + log log N ). To handle the case when D v ≤ 2 (log log N ) 2 we use an additional decomposition of the grammar and further reduce the problem to weighted ancestor queries on trees of small weighted height. Finally, we give an efficient solution to weighted ancestor for this specialized case that leads to our final result for fringe access.
Next, we use our fringe access result to obtain our solution to the static finger search problem. The key idea is to decompose the grammar into heavy paths as done by Bille et al. [10], which has the property that any root-to-leaf path in the directed acyclic graph representing the grammar consists of at most O(log N ) heavy paths. We then use this to compactly represent the finger as a sequence of the heavy paths. To implement access, we binary search the heavy paths in the finger to find an exit point on the finger, which we then use to find an appropriate node to apply our solution to fringe access on. Together with a few additional tricks this gives us Theorem 1(i).
Unfortunately, the above approach for the static finger search problem does not extend to the dynamic setting. The key issue is that even a tiny local change in the position of the finger can change Θ(log N ) heavy paths in the representation of the finger, hence requiring at least Ω(log N ) work to implement movefinger. To avoid this we give a new compact representation of the finger based on both heavy path and the special paths obtained from our van Emde Boas decomposition used in our fringe access data structure. We show how to efficiently maintain this representation during local changes of the finger, ultimately leading Theorem 1(ii).

Longest Common Extensions
As application of Theorem 1, we give an improved solution to longest common extension problem in grammar compressed strings. The first solution to this problem is due to Bille et al. [9]. They showed how to extend random access queries to compute Karp-Rabin fingerprints. Combined with an exponential search this leads to a linear space solution to the longest common extension problem using O(log N log ) time, where is the length of the longest common extension. We note that we can plug in any of the above mentioned random access solution. More recently, Nishimoto et al. [36] used a completely different approach to get O(log N + log log * N ) query time while using superlinear O(n log N log * N ) space. We obtain: Note that we need to verify the Karp-Rabin fingerprints during preprocessing in order to obtain a worst-case query time. Using the result from Bille et al. [10] this gives a randomized expected preprocessing time of O(N log N ). Theorem 2 improves the O(log N log ) solution to O(log N + log 2 ). The new bound is always at least as good and asymptotically better whenever = o(N ) where is a constant. The new result follows by extending Theorem 1 to compute Karp-Rabin fingerprints and use these to perform the exponential search from [9]. Due to lack of space the proof of Theorem 2 is deferred to the full version. A weighted tree has weights on its edges. A weighted ancestor query for node v and weight d returns the highest node w such that the sum of weights on the path from the root to w is at least d.
Grammars and Straight Line Programs. Grammar-based compression replaces a long string by a small context-free grammar (CFG). We assume without loss of generality that the grammars are in fact straight-line programs (SLPs). The lefthand side of a grammar rule in an SLP has exactly one variable, and the forighthand side has either exactly two variables or one terminal symbol. In addition, SLPs are unambigous and acyclic. We view SLPs as a directed acyclic graph (DAG) where each rule correspond to a node with outgoing ordered edges to its variables. Let S be an SLP. As with trees, we denote the left and right child of an internal node v by left(v) and right(v). The unique string S(v) of length N v is produced by a depth-first left-to-right traversal of v in S and consist of the characters on the leafs in the order they are visited. The corresponding parse tree for v is denoted T (v). We will use the following results, that provides efficient random access from any node v in S.  Fringe Access In this section we consider the fringe access problem. Here the goal is to compactly represent the SLP, such that for any node v, we can efficiently access locations in the string S(v) close to the start or the end of the substring. The fringe access problem is the key component in our finger search data structures. A straightforward solution to the fringe access problem is to apply a solution to the random access problem. For instance if we apply the random access solution from Bille et al. [10] stated in Lemma 3 we immediately obtain a linear space solution with O(log N v ) access time, i.e., the access time is independent of the distance to the start or the end of the string. This is an immediate consequence of the central grammar decomposition technique of [10], and does not extend to solve fringe access efficiently. Our main contribution in this section is a new approach that bypasses this obstacle. We show the following result.
The key idea in this result is a van Emde Boas style decomposition of S combined with a predecessor data structure on selected paths in the decomposition. To achieve linear space we reduce the predecessor queries on these paths to a weighted ancestor query. We first give a data structure with query time O((log log N ) 2 + log(min(i, N v − i))). We then show how to reduce the query time to O(log log N + log(min(i, N v − i))) by reducing the query time for small i. To do so we introduce an additional decomposition and give a new data structure that supports fast weighted ancestor queries on trees of small weighted height.
For simplicity and without loss of generality we assume that the access point i is closest to the start of S(v), i.e., the goal is to obtain O(log(i) + log log N ) time. By symmetry we can obtain the corresponding result for access points close to the end of S(v).

van Emde Boas Decomposition for Grammars
We first define the vEB decomposition on the parse tree T and then extend it to the SLP S. In the decomposition we use the ART decompostion by Alstrup et al. [1].
ART Decomposition. The ART decomposition introduced by Alstrup et al. [1] decomposes a tree into a single top tree and a number of bottom trees. Each bottom tree is a subtree rooted in a node of minimal depth such that the subtree contains no more than x leaves and the top tree is all nodes not in a bottom tree. The decomposition has the following key property.  Note that except for the nodes on the lowest level -which are not in any top tree -all nodes belong to exactly one top tree. For any node v ∈ T not in the last level, let T top (v) be the top tree v belongs to. The leftmost top path of v is the path from v to the leftmost leaf of T top (v). See Figure 1.
Intuitively, the vEB decomposition of T defines a nested hierarchy of subtrees that decrease by at least the square root of the size at each step.

The van Emde Boas Decomposition of Grammars.
Our definition of the vEB decomposition of trees can be extended to SLPs as follows. Since the vEB decomposition is based only on the length of the string N v generated by each node v, the definition of the vEB decomposition is also well-defined on SLPs. As in the tree, all nodes belong to at most one top DAG. We can therefore reuse the terminology from the definition for trees on SLPs as well.
To compute the vEB decomposition first determine the level of each node and then remove all edges between nodes on different levels. This can be done in O(n) time.

Data Structure
We first present a data structure that achieves O((log log N ) 2 + log(i)) time. In the next section we then show how to improve the running time to the desired O(log log(N ) + log(i)) bound. Our data structure contains the following information for each node v ∈ S. Let l 1 , l 2 , . . . , l k be the nodes hanging to the left of v's leftmost top path (excluding nodes hanging from the bottom node).
The length N v of S(v). The sum of the sizes of nodes hanging to the left of v's leftmost top path s v = |l 1 | + |l 2 | + . . . + |l k |. A pointer b v to the bottom node on v's leftmost top path. A predecessor data structure over the sequence 1, |l 1 | + 1, |l 1 | + |l 2 | + 1, . . . , k−1 i=1 |l i | + 1. We will later show how to represent this data structure.
In addition we also build the data structure from Lemma 3 that given any node v supports random access to S(v) in O(log N v ) time using O(n) space.

36:8 Finger Search in Grammar-Compressed Strings
Query. To perform an access query we proceed as follows. Suppose that we have reached some node v and we want to compute S(v) [i]. We consider the following five cases (when multiple cases apply take the first): (1). Decompress S(v) and return the i'th character.

2.
If i ≤ s v . Find the predecessor p of i in v's predecessor structure and let u be the corresponding node. Recursively find

5.
In all other cases, perform a random access for i in S(v) using Lemma 3.
To see correctness, first note that case (1) and (5) are correct by definition. Case (2) is correct since when i ≤ s v we know the i'th leaf must be in one of the trees hanging to the left of the leftmost top path, and the predecessor query ensures we recurse into the correct one of these bottom trees. In case (3) and (4) we check if the i'th leaf is either in the left or right subtree of b v and if it is, we recurse into the correct one of these.

Compact Predecessor Data Structures.
We now describe how to represent the predecessor data structure. Simply storing a predecessor structure in every single node would use O(n 2 ) space. We can reduce the space to O(n) using ideas similar to the construction of the "heavy path suffix forest" in [10].
Let L denote the leftmost top path forest. The nodes of L are the nodes of S. A node u is the parent of v in L iff u is a child of v in S and u is on v's leftmost top path. Thus, a leftmost top path v 1 , . . . , v k in S is a sequence of ancestors from v 1 in L. The weight of an edge (u, v) in L is 0 if u is a left child of v in S and otherwise N left (v) . Several leftmost top paths in S can share the same suffix, but the leftmost top path of a node in S is uniquely defined and thus L is a forest. A leftmost path ends in a leaf in the top DAG, and therefore L consists of O(n) trees each rooted at a unique leaf of a top dag. A predecessor query on the sequence 1, |l 1 | + 1, |l 1 | + |l 2 | + 1, . . . , k−1 i=1 |l i | + 1 now corresponds to a weighted ancestor query in L. We plug in the weighted ancestor data structure from Farach-Colton and Muthukrishnan [18], which supports weighted ancestor queries in a forest in O(log log n + log log U )) time with O(n) preprocessing and space, where U is the maximum weight of a root-to-leaf path and n the number of leaves. We have U = N and hence the time for queries becomes O(log log N ).  (2) is O(log log N ) since we perform exactly one predececssor query in the predecessor data structure.

Space and Preprocessing
In case (5) we make a random access query in a node of size N v . From Lemma 3 we have that the query time is O(log N v ). We know level(v) = level(b v ) since they are on the same leftmost top path. From the definition of the level it follows for any pair of nodes u and w with the same level that N u ≥ √ N w and thus N bv ≥ √ N v . From the conditions we have

log i) and thus the running time for case (5) is O(log N v ) = O(log i).
Case (1) and (5) terminate the algorithm and can thus not happen more than once. Case (2), (3) and (4) are repeated at most O(log log N ) times since the level of the node we recurse on increments by at least one in each recursive call, and the level of a node is at most  O(log log N ). The overall running time is therefore O((log log N ) 2 + log i).
In summary, we have the following result.

Improving the Query Time for Small Indices
The above algorithm obtains the running time O(log i) for i ≥ 2 (log log N ) 2 . We will now improve the running time to O(log log N + log i) by improving the running time in the case when i < 2 (log log N ) 2 .
In addition to the data structure from above, we add another copy of the data structure with a few changes. When answering a query, we first check if i ≥ 2 (log log N ) 2 . If i ≥ 2 (log log N ) 2 we use the original data structure, otherwise we use the new copy.
The new copy of the data structure is implemented as follows. In the first level of the ARTdecomposition let x = 2 (log log N ) 2 instead of √ N . For the rest of the levels use √ x as before. Furthermore, we split the resulting new leftmost top path forest L into two disjoint parts: L 1 consisting of all nodes with level 1 and L ≥2 consisting of all nodes with level at least 2. For L 1 we use the weighted ancestor data structure by Farach-Colton and Muthukrishnan [18] as in the previous section using O(log log n + log log N )) = O(log log N ) time. However, if we apply this solution for L ≥2 we end up with a query time of O(log log n + log log x)), which does not lead to an improved solution. Instead, we present a new data structure that supports queries in O(log log x) time.

Lemma 7 (see full version). Given a tree T with n leaves where the sum of edge weights on any root-to-leaf path is at most x and the height is at most x, we can support weighted ancestor queries in O(log log x) time using O(n) space and preprocessing time.
We reduce the query time for queries with i < 2 (log log N ) 2 using the new data structure. The level of any node in the new structure is at most O(1 + log log 2 (log log N ) 2 ) = O(log log log N ). A weighted ancestor query in L 1 takes time O(log log N ). For weighted ancestor queries in L ≥2 , we know any node v has height at most 2 (log log N ) 2 and on any root-to-leaf path the sum of the weights is at most 2 (log log N ) 2 . Hence, by Lemma 7 we support queries in O(log log 2 (log log N ) 2 ) = O(log log log N ) time for nodes in L ≥2 .
We make at most one weighted ancestor query in L 1 , the remaining ones are made in L ≥2 , and thus the overall running time is O(log log N +(log log log N ) 2 +log i) = O(log log N +log i).
In summary, this completes the proof of Lemma 4.

Static Finger Search
We now show how to apply our solution to the fringe access to a obtain a simple data structure for the static finger search problem. This solution will be the starting point for solving the dynamic case in the next section, and we will use it as a key component in our result for longest common extension problem. Similar to the fringe search problem we assume without loss of generality that the access point i is to the right of the finger. Illustration of the data structure for a finger pointing at f and an access query at location i. h1, h2, h3 are the heavy paths visited when finding the finger. u corresponds to N CA(v f , vi) in the parse tree and hs is the heavy path on which u lies, which we use to find u. a is a value calculated during the access query.
Data Structure. We store the random access data structure from [10] used in Lemma 3 and the fringe search data structures from above. Also from [10] we store the data structure that for any heavy path h starting in a node v and an index i of a leaf in T (v) gives the exit-node from h when searching for i in O(log log N ) time and uses O(n) space.
To represent a finger the key idea is store a compact data structure for the corresponding root-to-leaf path in the grammar that allows us to navigate it efficiently. Specifically, let f be the position of the current finger and let p = v 1 . . . v k denote the path in S from the root to v f (v 1 = root and v k = v f ). Decompose p into the O(log N ) heavy paths it intersects, and call these h . Let l j be the index of f in S(v(h j )) and r j = N v(hj ) − l j . For the finger we store: 1. The sequence r 1 , r 2 , . . . , r j (note r 1 ≤ r 2 ≤ · · · ≤ r j ).

The sequence
Analysis. The random access and fringe search data structures both require O(n) space. Each of the 3 bullets above require O(log N ) space and thus the finger takes up O(log N ) space. The total space usage is O(n).

Setfinger.
We implement setfinger(f ) as follows. First, we apply Lemma 3 to make random access to position f . This gives us the sequence of visited heavy paths which exactly corresponds to h j , h j−1 , . . . , h 1 including the corresponding l i values from which we can calculate the r i values. So we update the r i sequence accordingly. Finally, decompress and save the string The random access to position f takes O(log N ) time. In addition to this we perform a constant number of operations for each heavy path h i , which in total takes O(log N ) time. Decompressing a string of log N characters can be done in O(log N ) time (using [10]). In total, we use O(log N ) time.

Access.
To perform access(i) (i > f ), there are two cases. If D = i − f ≤ log N we simply return the stored character F T [D] in constant time. Otherwise, we compute the node u = nca(v f , v i ) in the parse tree T as follows. First find the index s of the successor to D in the r i sequence using binary search. Now we know that u is on the heavy path h s . Find the exit-nodes from h s when searching for respectively i and f using the data structure from [10] -the topmost of these two is u. See Fig. 2. Finally, we compute a as the index of f in T (left(u)) from the right and use the data structure for fringe search from Lemma 4 to

Dynamic Finger Search
In this section we show how to extend the solution from Section 4 to handle dynamic finger search. The target is to support the movefinger operation that will move the current finger, where the time it takes is dependent on how far the finger is moved. Obviously, it should be faster than simply using the setfinger operation. The key difference from the static finger is a new decomposition of a root-to-leaf path into paths. The new decomposition is based on a combination of heavy paths and leftmost top paths, which we will show first. Then we show how to change the data structure to use this decomposition, and how to modify the operations accordingly. Finally we shortly describe how to generalize the solution to work when movefinger/access might both be to the left and right of the current finger. Before we start, let us see why the data structure for the static finger cannot directly be used for dynamic finger. Suppose we have a finger pointing at f described by Θ(log N ) heavy paths. It might be the case that after a movefinger(f + 1) operation, it is Θ(log N ) completely different heavy paths that describes the finger. In this case we must do Θ(log N ) work to keep our finger data structure updated. This can for instance happen when the current finger is pointing at the right-most leaf in the left subtree of the root.
Furthermore, in the solution to the static problem, we store the substring S[f +1, f +log N ] decompressed in our data structure. If we perform a movefinger(f + log N ) operation nothing of this substring can be reused. To decompress log N characters takes Ω(log N ) time, thus we cannot do this in the movefinger operation and still get something faster than Θ(log N ).

Left Heavy Path Decomposition of a Path
. v b is part of a heavy path and v b+1 is not on the same heavy path. Similarly, A left heavy path decomposition is a decomposition of a root-to-leaf path p into an arbitrary sequence p 1 . . . p j of maximal heavy subpaths, maximal leftmost top subpaths and (non-maximal) leftmost top subpaths immediately followed by maximal heavy subpaths.
Define v(p i ) as the topmost node on the subpath p i . Let l j be the index of the finger f in S(v(p j )) and r j = N v(pj ) − l j . Let t(p i ) be the type of p i ; either heavy subpath (HP ) or leftmost top subpath (LT P ).
A left heavy path decomposition of a root-to-leaf path p is not unique. The heavy path decomposition of p is always a valid left heavy path decomposition as well. The visited heavy paths and leftmost top paths during fringe search are always maximal and thus is always a valid left heavy path decomposition.

36:12
Finger Search in Grammar-Compressed Strings Lemma

The number of paths in a left heavy path decomposition is O(log N ).
Proof. There are at most O(log N ) heavy paths that intersects with a root-to-leaf path (Lemma 3). Each of these can at most be used once because of the maximality. So there can at most be O(log N ) maximal heavy paths. Each time there is a maximal leftmost top path, the level of the following node on p increases. This can happen at most O(log log N ) times. Each non-maximal leftmost top path is followed by a maximal heavy path, and since there are only O(log N ) of these, this can happen at most O(log N ) times. Therefore the sequence of paths has length O(log N + log log N + log N ) = O(log N ).

Data Structure
We use the data structures from [10] as in the static variant and the fringe access data structure with an extension. In the fringe access data structure there is a predecessor data structure for all the nodes hanging to the left of a leftmost top path. To support access and movefinger we need to find a node hanging to the left or right of a leftmost top path. We can do this by storing an identical predecessor structure for the accumulated sizes of the nodes hanging to the right of each leftmost top path. Again, the space usage for this predecessor structure can be reduced to O(n) by turning it into a weighted ancestor problem.
To represent a finger the idea is again to have a compact data structure representing the root-to-leaf path corresponding to the finger. This time we will base it on a left heavy path decomposition instead of a heavy path decomposition. Let f be the current position of the finger. For the root-to-leaf path to v f we maintain a left heavy path decomposition, and store the following for a finger: 1. The sequence r 1 , r 2 , . . . , r j (r 1 ≤ r 2 ≤ · · · ≤ r j ) on a stack with the last element on top.

Movefinger.
To move the finger we combine the access and setfinger operations. Find the index s of the successor to D = i − f in r 1 , r 2 , . . . , r j using binary search. Now we know u = nca(v i , v f ) must lie on p s . Find u in the same way as when performing access. From all of the stacks pop all elements above index s. Compute a as the index of f in S(left(u)) from the right. The finger should be moved to index i − f − a in right (u). First look at the heavy path right(u) lies on and find the proper exit-node w using the data structure from [10]. Then continue with fringe searh from the proper child of w. This gives a heavy path followed by a sequence of maximal leftmost top paths and heavy paths needed to reach v i from right(u), push the r j , v(p j ), and t(p j ) values for these on top of the respective stacks.
We now verify the sequence of paths we maintain is still a valid left heavy path decomposition. Since fringe search gives a sequence of paths that is a valid left heavy path decomposition, the only problem might be p s is no longer maximal. If p s is a heavy path it will still be maximal, but if p s is a leftmost top path then level(u) and level(right(u)) might be equal. But this possibly non-maximal leftmost top path is always followed by a heavy path. Thus the overall sequence of paths remains a left heavy path decomposition. The

Moving/Access to the Left of the Dynamic Finger
Previously we have assumed i > f , we will now show how this assumption can be removed. It is easy to see we can mirror all data structures and we will have a solution that works for i < f instead. Unfortunately, we cannot just use a copy of each independently, since one of them only supports moving the finger to the left and the other only supports moving to the right. We would like to support moving the finger left and right arbitrarily. This was not a problem with the static finger since we could just make setfinger in both the mirrored and non-mirrored data structures in O(log N ) time.
Instead we extend our finger data structure. First we extend the left heavy path decomposition to a left right heavy path decomposition by adding another type of paths to it, namely rightmost top paths (the mirrorred version of leftmost top paths). Thus a left right heavy path decomposition is a decomposition of a root-to-leaf path p into an arbitrary sequence p 1 . . . p j of maximal heavy subpaths, maximal leftmost/rightmost top subpaths and (non-maximal) leftmost/rightmost top subpaths immediately followed by maximal heavy subpaths. Now t(p i ) = HP |LT P |RT P . Furthermore, we save the sequence l 1 , l 2 , . . . , l j (l j being the left index of f in T (v(p i ))) on a stack like the r 1 , r 2 , . . . , r j values, etc.
When we do access and movefinger where i < f , the subpath p s where nca(v f , v i ) lies can be found by binary search on the l j values instead of the r j values. Note the l j values are sorted on the stack, just like the r j values. The following heavy path lookup/fringe access should now be performed on left(u) instead of right(u). The remaining operations can just be performed in the same way as before.

36:14
Finger Search in Grammar-Compressed Strings