<<

In-Place Initializable Arrays

Takashi Katoh1 and Keisuke Goto1

1Fujitsu Laboratories Ltd., Kawasaki, Japan., kato.takashi [email protected], [email protected]

Abstract An initializable array is an array that supports the read and write operations for any element and the initialization of the entire array. This paper proposes a simple in-place algorithm to implement an initializable array of length N containing ` ∈ O(w) entries on the word RAM model with w bits word size, which supports all the operations in constant worst-case time. The algorithm requires N` + 1 bits in total, namely, which requires only 1 extra on top of a normal array of length N containing ` bits entries. The time and space complexities are optimal since it was already proven that there is no implementation of an initializable array with no extra bit supporting all the operations in constant worst-case time [Hagerup and Kammer, ISAAC 2017]. Our algorithm significantly improves upon the best algorithm presented in the earlier studies [Navarro, CSUR 2014] which uses N + o(N) extra bits to support all the operations in constant worst-case time.

1 Introduction

Arrays are important data structures that support the fundamental read and write operations of any given element in constant worst-case time. Another fundamental operation known as ini- tialization, which writes a given initial value to all the elements of the array, appears frequently in numerous algorithms and programs. Although initialization is naively implemented by lin- ear time write operations, the naive initialization may cause a bottleneck in the applications which use large arrays and frequently require initialization. The issue motivates us to study a fundamental initializable array. An initializable array Z[0 ...N − 1] of length N containing ` bits entries supports the fol- lowing operations: read, write, and initialization.

• read(i): Return the value stored in the i-th element of Z.

• write(i, v): the i-th element of Z to v. arXiv:1709.08900v4 [cs.DS] 9 Mar 2021 • init(v): Set all the elements of Z to v.

where i and v are the integers within 0 ≤ i < N and 0 ≤ v < 2`. read(i) and write(i, v) are also denoted by Z[i], Z[i] ← v, respectively. A normal array is obviously an initializable array since it fundamentally supports the read and write operations in constant worst-case time, and init can be implemented by calling write for all N positions in Θ(N) time. Note that init does not necessarily have to call write N times, and it only has to behave as if it does that. That is, when read(i) is called, it only has to return the initial value v of the last initialization init(v) if write(i, v0) was not called after the last initialization; otherwise, it returns v0 of the last write write(i, v0). We assume the word RAM model with w ∈ Ω(log N) bits word size that usual arithmetic and bitwise operations on a word take constant worst-case time, and we also assume that

1 ` ∈ O(w). We focus on and evaluate the additional extra space over N` bits because the N` bits space is the trivial lower bound. Moreover, we account only the dynamic values for the space of algorithms, e.g., the space for an initial value or auxiliary arrays. Conversely, we do not account static values that can be embedded into a program, e.g., the space for the length of the array or certain static parameters of algorithms. Initializable arrays have been studied since the 1970s. A folklore algorithm supporting all the operations in constant worst-case time was first mentioned (but not described) in the study by Aho et al. [1, Ex. 2.12]. The complete description was later presented in the studies by Mehlhorn [12, Sec. III.8.1] and Bentley [2, Column 1]. The most technical point of implementing the efficient initializable arrays is how to memorize information whether each element of Z was overwritten after the last initialization. The folklore algorithm memorizes the information by using the chain technique, which represents bi-directional links in two auxiliary arrays. It requires 2`(N +1) extra bits. Navarro [15,16] reduced the space to N +o(N) extra bits without increasing the time complexities. Their algorithm combined the folklore algorithm with a technique using a bit array B of length N such that B[i] is 1 if and only if the i-th element of the array has been written from the last initialization. The runtime of an algorithm depends on the access frequency to the array, which is the ratio of the number of read and write operations, to the array length. Fredriksson and Kilpel¨ainen[3] measured the runtime performances of several algorithms. According to their computational experiments, the folklore algorithm and Navarro’s algorithm present the highest efficiency when the access frequency is low (below 1%), while the bitmap solution and the naive solution present the highest efficiency when the access frequency is within 1–10% and greater than 10%, respec- tively. The construction of ZDD [13] is a good example to demonstrate the effectiveness of the initializable arrays. ZDD is a space-efficient data structure which represents any family of sets and is widely used for various practical applications [14, 17]. Knuth [9, 10] used the folklore algorithm to implement a large in the fast ZDD construction algorithm Simpath.A hash table is used to represent million of nodes of ZDD and is initialized before each step of the algorithm. Initializable arrays realize the efficient initialization of such a large hash table.

Our Contributions We propose a simple in-place algorithm for initializable arrays, and we have the following theorem.

Theorem 1. There exists an initializable array Z of length N containing ` ∈ O(w) bits entries which requires 1 extra bit and supports the operations, read, write, and initalization in constant worst-case time.

The algorithm uses a novel in-place chain technique which is nearly identical to the folklore algorithm but works in-place. The time and space complexities are optimal since there is no implementation of an initializable array with no extra bit supporting all the operations in constant worst-case time [7]. Moreover, the algorithm is extremely simple and the pseudo-code of the core idea is written within 80 lines (see Algorithm 1–3).

Recent Works Hagerup and Kammer [7], and Loong et al. [11] have also proposed in-place algorithms for initializable arrays recently and independently from us. Hagerup and Kammer’s algorithm supports the read/write operations in O(t) worst-case time, the initialization in con- stant worst-case time using extra dN/(w/ct)te bits, where is a constant value greater than 1, and t is a time and space trade-off parameter within 1 ≤ t ≤ dlog Ne. The in-place algorithm of 1 extra bit space is obtained by setting t = dlog Ne, but the read and write operations take O(log N) worst-case time. Loong et al. proposed two algorithms, both of which use 1 extra bit and support the read/initialization operations in constant worst-case time, and for write, one of which, takes amortized constant time and the other takes constant worst-case expected

2 Algorithms Extra bits init read write Normal array 0 Θ(N) O(1) O(1) Folklore [1, 2, 12] 2`(N + 1) O(1) O(1) O(1) Navarro [15, 16] N + o(N) O(1) O(1) O(1) Hagerup and Kammer [7] dN/(w/ct)te O(1) O(t) O(t) Loong et al. [11] 1 O(1) O(1) amortized/expected O(1) This paper 1 O(1) O(1) O(1)

Table 1: Comparison of the time and space complexities between the earlier studies, recent ones but independent from us, and ours. In [7], c > 1 is a constant value and t is a time and space trade off parameter within 1 ≤ t ≤ dlog Ne, and the algorithm requires 1 extra bit when setting t = dlog Ne. Only write by Loong et al. takes amortized time or worst-case expected time, and the other operations take worst-case time. time. Compared to these algorithms, our algorithm is quite simple and runs in optimal time and space. See also Table 1. Several space-efficient algorithms have been proposed based on the preprint version of this paper 1. Kammer and Sajenko [8] have extended our algorithm to implement dynamic initializ- able arrays which can increase and decrease the array size. Hagerup [4,5] used the in-place chain techniques presented in this paper to implement space-efficient choice dictionaries which can re- turn an arbitrary element stored after the initialization. These studies for highly space-efficient data structures were introduced in [6].

Organizations The rest of the paper is organized as follows. Section 2 introduces the folklore algorithm which our algorithm is based on. Section 3 considers a simple problem setting ` ≥ dlog Ne and ` ∈ O(w), and proposes in-place algorithm using 2` extra bits for this problem setting. Section 4 considers a more general problem setting ` ∈ O(w), and presents the proof of Theorem 1.

2 Folklore Algorithm

The folklore algorithm implements an initializable array Z of length N containing ` bits entries for ` ≥ dlog Ne and ` ∈ O(w), which supports all the operations in constant worst-case time. The algorithm uses three normal arrays of length N containing ` bits entries, V, F, and T 2, along with two variables of ` bits, an initial value initv and a stack pointer b, and it thus requires 2`(N + 1) extra bits in total. initv stores the initial value, T is used as a stack, and b indicates the stack size of T. We say that F[i] and T[j] are chained when they are linked to each other, namely, F[i] = j, T[j] = i, and j < b. V[i] stores a written value, and we maintain the invariant that Z[i] = V[i] if F[i] is chained, and Z[i] = initv otherwise. The algorithm implements each operation using the invariant as follows:

• read(i): Return V[i] if F[i] is chained, and initv otherwise.

• write(i, v): Set V[i] to v, and if F[i] is unchained, create a new chain between F[i] and T[b] by setting T[b] ← i, F[i] ← b, and b ← b + 1.

• init(v): Break all chains by setting b ← 0 and update the initial value initv ← v. read is trivially obtained from the invariant. write creates a new chain of F and T only when an element is written for the first time, and thus the number of chains is at most N, and the chain will never be broken until init is called. init breaks all the chains by setting

1The preprint version of this paper is available at https://arxiv.org/abs/1709.08900 2V, F, and T stand for Value, From, and To, respectively.

3 Figure 1: Four blocks chained or unchained in the unwritten chained area (UCA) or the written chained area (WCA) in A. Bold borders indicate blocks. Blocks Bi2 and Bi3 are chained since they are in the different areas, A[2i3] = F[2i3] = 2i2 and A[2i2] = T[2i2] = 2i3. b ← 0, and thus it implies that all the elements of Z are initialized by a new initial value initv. Each operation takes constant worst-case time. The folklore algorithm thus maintains the invariant and implements an initializable array Z using 2`(N + 1) extra bits, and supports all the operations in constant worst-case time.

3 In-Place Initializable Arrays for a Simple Setting

We propose an algorithm which implements an initializable array Z for ` ≥ dlog Ne and ` ∈ O(w). The algorithm uses one normal array A of N` bits and two variables, an initial value initv of ` bits and a stack pointer b of ` bits; it thus requires 2` extra bits. Section 4 then shows that the algorithm can be modified to run in optimal time and space for a more general setting. In the rest, we only consider the case N is even since, if N is odd, we just treat Z[N − 1] = A[N − 1]. The underlying concept of our algorithm is nearly identical to that of the folklore algorithm. Our algorithm also uses V, F, and T, but sparsely embeds them into A. This idea intuitively seems impossible because all 3N elements of V, F, and T are required in the worst case in the folklore algorithm, and the space of A is not sufficient to store all of them. Hence, we reduce the number of chains to solve this issue. Firstly, we split A into N/2 blocks of block size 2 and create chains between two blocks instead of two elements. Secondly, we also split A into two areas A[0 ... 2b − 1] and A[2b ...N − 1], and manage written and unwritten blocks in a different manner. In the first area, a block is chained if and only if the elements of the block has not been written from the last initialization. In the second area, a block is chained if and only if the elements of the block has been written from the last initialization. These two areas are called unwritten chained area (UCA) and written chained area (WCA), respectively. This idea is derived from the important observation that if the written elements are managed by chains (like the folklore algorithm), a few chains are required at the beginning after the last initialization, but this increases gradually and eventually reaches N. Conversely, if the unwritten elements are managed by chains, a few chains are required at the ending after the last initialization, but approximately N chains are required at the beginning. Our algorithm uses these two different management approaches in two areas of A by changing the size of the area dynamically. Here, the threshold of the areas 2b is set to a position such that the number of chains is the least, namely, the number of unwritten blocks in UCA and the number of written blocks in WCA are equaled. The memory layout of A is shown in Figure 1. Let Bi = A[2i . . . 2i + 1] be the i-th block. Each A[i] belongs to the block Bbi/2c. We say that blocks Bi and Bj are chained if A[2i] = 2j and A[2j] = 2i and neither of the blocks are in the same area. Note that any element can store any index of A since ` ≥ dlog Ne. There are four types of blocks which are classified written or

4 unwritten blocks located in UCA or WCA. For each type of block, our algorithm maintains the following four invariants, where V[i], F[i], and T[i] respectively represent the functional aspects of A[i] as in the folklore algorithm.

1. Block Bi1 is a written block in UCA ⇔ Bi1 is not chained to any block. It holds (A[2i1], A[2i1 + 1]) = (V[2i1], V[2i1 + 1]) and (Z[2i1], Z[2i1 + 1]) = (V[2i1], V[2i1 + 1]).

2. Block Bi2 is an unwritten block in UCA ⇔ Bi2 is chained to a block Bi3 in WCA. It holds (A[2i2], A[2i2 + 1]) = (T[2i2], V[2i3]) and (Z[2i2], Z[2i2 + 1]) = (initv, initv).

3. Block Bi3 is a written block in WCA ⇔ Bi3 is chained to a block Bi2 in UCA. It holds

(A[2i3], A[2i3 + 1]) = (F[2i3], V[2i3 + 1]) and (Z[2i3], Z[2i3 + 1]) = (V[2i3], V[2i3 + 1] =

A[2i2 + 1]).

4. Block Bi4 is an unwritten block in WCA ⇔ Bi4 is not chained to any block. (A[2i4], A[2i4+ 1]) are any values but A[2i4] must not link to A[2j] in UCA each other, and it holds (Z[2i4], Z[2i4 + 1]) = (initv, initv). The implementation of our algorithm in each operation is described as follows. read is trivially implemented by the invariants. init is implemented similar to the folklore algorithm by setting b and initv to zero and a given initial value, respectively. The pseudo-codes of init and read are described in Algorithm 2 in Appendix. write is more complicated than read and init since it may break the invariants by writing a new value. The following tools 3 is used to implement write (their pseudo-codes are described in Algorithm 3 in Appendix).

• chainedTo(Bi): Return the block chained to Bi if Bi is chained, and return a symbol None otherwise.

• makeChain(Bi,Bj): Make a new chain between Bi in UCA and Bj in WCA.

• breakChain(Bi): Break the chain of the block Bi in UCA if Bi is chained, and do nothing otherwise.

• initBlock(Bi): Initialize the block Bi with initv, namely, write initv to A[2i] and A[2i+ 1].

• extend(): Extend UCA by one block and return an unwritten block in UCA that has not yet been chained and is initialized with (initv, initv). chainedTo, makeChain, and initBlock are directly implemented from their functional aspects. breakChain breaks an unexpected chain between Bi and Bk by setting A[2k] ← 2k to keep Bk be unchained regardless of the value of Bi. extend updates b ← b + 1 and change the area of Bb−1 from WCA to UCA. There are two cases where Bb−1 is an unwritten block or written block: (1) If Bb−1 is an unwritten block and is not chained to any block, it is initialized by calling initBlock(Bb−1). This initialization may make an unexpected chain between Bb−1 in UCA and a block Binitv/2 in WCA, that is, initv is even, initv ≥ 2b, A[2(b − 1)] = initv, and A[initv] = 2(b − 1). To fix this unexpected chain, we call breakChain(Bb−1), and then return the unwritten block Bb−1. (2) If Bb−1 is a written block and chained to an unwritten block Bk in UCA before extending, we change the block layouts of Bb−1 and Bk following the invariants. We simply write Z[2(b −1)] and Z[2(b −1)+1] into A[2(b −1)] and A[2(b −1)+1], respectively, and call initBlock(Bk). This change may make unexpected chains for Bb−1 and Bk. To fix these unexpected chains, we call breakChain(Bb−1) and breakChain(Bk), and then return the unwritten block Bk. 3Some of these functions take and return blocks as their arguments and outputs, respectively. Actual imple- mentations treat such blocks as pointers, so copy and comparison of the constant number of blocks take constant worst-case time. However, in our pseudo-codes, we represent a block Bi as just Bi instead of a pointer i to emphasize that we are indicating a block.

5 Algorithm 1: write(i, v) 1 Function write(i, v): 0 2 i ← bi/2c // Bi0 is the block that contains A[i]. 3 Bk ← chainedTo(Bi); 4 if i0 < b then 5 if Bk = None then // Bi0 is a written block in UCA. 6 A[i] ← v ; 7 breakChain(Bi0 ); 8 else // Bi0 is an unwritten block in UCA. 9 Bj ← extend() ; 10 if Bi0 = Bj then // The same situation of just before Line 6. 11 We perform the same procedure as in Lines 6–7. ; 12 else // Swap Bi0 for Bj 13 (A[2j], A[2j + 1]) ← (A[2i0], A[2i0 + 1]) ; 14 makeChain(Bj,Bk); 15 initBlock(Bi0 ); // The same situation of just before Line 6. 16 We perform the same procedure as in Lines 6–7. ; 17 else 18 if Bk 6= None then // Bi0 is written block in WCA. 19 if i mod 2 = 0 then // Write v to the second element of Bk 20 A[2k + 1] ← v ; 21 else 22 A[i] ← v // Write v to the second element of Bi0 23 else // Bi0 is an unwritten block in WCA. 24 Bk ← extend() ; 25 if Bi0 = Bk then // The same situation of just before Line 6. 26 We perform the same procedure as in Lines 6–7. ; 27 else 28 initBlock(Bi0 ); 29 makeChain(Bk,Bi0 ); // The same situation of just before Line 19. 30 We perform the same procedure as in Lines 19–22. ;

6 The pseudo-code of write is described in Algorithm 1. When write(i, v) is called, there are 0 four major conditions of the block Bi0 for i = bi/2c, and we write v while keeping the invariants in each state as follows.

• Bi0 is a written block in UCA (Lines 6–7). Z[i] has already been written, and thus we simply rewrite it with a new value v. We expect Bi0 is unchained from the invariant, but Bi0 may be accidentally chained to a block by writing v to A[i]. We call breakChain(Bi0 ) to break such an unexpected chain.

• Bi0 is an unwritten block in UCA and is chained to a block in WCA (Lines 9–16). Since Bi0 is chained, there is not sufficient space to store v. To overcome this issue, we extend UCA, obtain an unwritten block Bj in UCA that has not yet been chained, swap Bj for Bi0 , and write v to A[i] in the block Bi0 . There are two major concerns in the procedure: (1) Bi0 may be equal to Bj before swapping. (2) Bi0 may be accidentally chained to a block by writing v to A[i], which is the same situation in Lines 6–7. In case 1, we do not swap Bj for Bi0 , and simply write v to A[i]. In case 2, we break an unexpected chain by breakChain(Bi0 ).

• Bi0 is a written block in WCA and is chained to a block Bk in UCA (Lines 19–22). Z[i] has already been written, and thus we simply write v in the corresponding position A[i] or A[2k + 1].

• Bi0 is an unwritten block in WCA (Lines 24–30). Z[i] has been unwritten, and thus we have to make a chain between the block Bi0 and a block in UCA. We extend UCA and obtain a new initialized block Bk in UCA by calling extend. If Bi0 = Bk, Bi0 is now located in UCA and it is the same situation in Lines 6–7, and then we do the same procedure. Otherwise, we initialize the block Bi0 and make the chain between Bk and Bi0 . It is now the same situation in Lines 19–22, so we do the same procedure.

Roughly speaking, our algorithm extends UCA (suppressing WCA) by increasing b by one when writing a new value. This is similar to how a normal array initializes itself by writing a value from left to right. Our algorithm performs the same operation in a lazy manner, that is, it writes only two values when increasing b. In the extreme case, where 2b = N, all the elements have already been written, and the contents of A are completely equal to Z , that is, A[i] = Z[i] for all 0 ≤ i < N. Therefore, our algorithm maintains the invariants during the operations, and supports all the operations in constant worst-case time using only 2` extra bits.

4 In-Place Initializable Arrays for a General Setting

The algorithm in Section 3 can be modified so that it requires only 1 extra bit and that it runs in a more general problem setting ` ∈ O(w), rather than ` ≥ dlog Ne and ` ∈ O(w). Therefore, we have Theorem 1. We firstly describe the former modification in the same problem setting in Section 3, and then describe the latter one. We can reduce the space requirement to 1 extra bit by embedding initv and b into A. We use only a 1 bit variable flag, and set flag = 1 if and only if the size of WCA is zero, that is, Z = A. We change the block size to 4, and when init(v) is called, we also initialize the last block of A by v. This implies that, if flag = 0, the last block is always a written block in WCA and we can store any two ` bits values in the block without breaking the invariants of our algorithm. We store initv and b in that space. See the new layout of the blocks in Figure 4. Our algorithm runs similar to the algorithm of block size 2 if flag = 0, and runs as the normal array if flag = 1. This modification does not worse the time complexities for all the operations.

7 Unwritten Chained Area (UCA) 4b Written Chained Area (WCA) (2) unwritten block (3) written block Bi2 Bi3

index 4i2 4i2+1 4i2+2 4i2+3 4i3 4i3+1 4i3+2 4i3+3

A T[4i2] V[4i3] V[4i3+1] V[4i3+2] F[4i3] initv b V[4i3+3]

index 4i2 4i2+1 4i2+2 4i2+3 4i3 4i3+1 4i3+2 4i3+3

Z initv initv initv initv V[4i3] V[4i3+1] V[4i3+2] V[4i3+3]

Figure 2: The layout of A and Z for block size 4, which is related to Figure 1. The unwritten block Bi2 in UCA and the written block Bi3 in WCA are chained. The second and third elements of Bi3 can store any ` bits values, respectively. If flag = 0, the second and third elements of the last block (written block in WCA) store initv and b, respectively. The second and third elements of other written blocks in WCA are unused and store unknown values, and they do not affect the algorithm behavior.

We can modify the algorithm so that it runs for the more general setting, ` ∈ O(w) rather than ` ≥ dlog Ne and ` ∈ O(w) without worsening both the time and space complexities. If ` < dlog Ne, A[i] cannot store a pointer to a position in A and so we cannot use the in-place chain technique as Section 3. To solve this issue, we simulate an initializable array Z by using another initializable array X containing larger bit size entries and a normal array Y. Let dlog Ne 0 N 0 0 p = ` e, N = b p c, ` = p`, and c = N mod p = N − pN . X is an initializable array of 0 0 Pp−1 `(p−1−j) length N containing ` bits entries such that X[i] = j=0 2 Z[ip + j] has a bit pattern corresponding to the concatenation of the bit patterns (Z[ip],..., Z[(i+1)p−1]). Y is a normal array of length c containing ` bits entries such that Y[i] is equal to Z[N 0 + i] for 0 ≤ i < c. X can be implemented with only 1 extra bit as described earlier since dlog Ne ≤ `0 < dlog Ne + `. We describe only how X simulates Z[0,...,N 0 − 1] since Y can simulate the remaining part of Z based on the same concept. The read and write for Z[i] can be performed by reading and writing to X[bi/pc] with the constant number of bit operations. When init(initv) is called on Z, we call init(initv 0) on X, where initv 0 has a bit pattern corresponding to the concatenation of p consecutive initial values of initv. Note that initv 0 can be computed by multiplying initv and the pre-computed static bit pattern 4 of length `0 whose each `-th bit from the left is 1 and others are 0. Therefore, Z of ` ∈ O(w) bits entries can be implemented using 1 extra bit space, and we have Theorem 1.

Acknowledgements

We would like to thank Shunsuke Inenaga and Hideo Bannai for many constructive suggestions, and anonymous reviewers for their insightful comments.

References

[1] Alfred V. Aho, John E. Hopcroft, and Jeffrey D. Ullman. The Design and Analysis of Computer Algorithms. Addison-Wesley, 1974.

[2] Jon Louis Bentley. Programming pearls. Addison-Wesley, 1986.

4The pre-computed static bit pattern is embedded within the program.

8 [3] Kimmo Fredriksson and Pekka Kilpel¨ainen.Practically efficient array initialization. Soft- ware: Practice and Experience, 46(4):435–467, 2016. doi:10.1002/spe.2314.

[4] Torben Hagerup. A constant-time colored choice dictionary with almost robust iteration. In Proceedings of the 44th International Symposium on Mathematical Foundations of Com- puter Science (MFCS 2019), volume 138 of LIPIcs, pages 64:1–64:14. Schloss Dagstuhl - Leibniz-Zentrum f¨urInformatik, 2019. doi:10.4230/LIPIcs.MFCS.2019.64.

[5] Torben Hagerup. Fast breadth-first search in still less space. In Proceedings of the 45th International Workshop on Graph-Theoretic Concepts in Computer Science (WG 2019), volume 11789 of Lecture Notes in Computer Science, pages 93–105. Springer, 2019. doi: 10.1007/978-3-030-30786-8\_8.

[6] Torben Hagerup. Highly succinct dynamic data structures. In Proceedings of the 22nd International Symposium on Fundamentals of Computation Theory (FCT 2019), volume 11651 of Lecture Notes in Computer Science, pages 29–45. Springer, 2019. doi:10.1007/ 978-3-030-25027-0\_3.

[7] Torben Hagerup and Frank Kammer. On-the-fly array initialization in less space. In Proceedings of the 28th International Symposium on Algorithms and Computation (ISAAC 2017), pages 44:1–44:12, 2017. doi:10.4230/LIPIcs.ISAAC.2017.44.

[8] Frank Kammer and Andrej Sajenko. Extra space during initialization of succinct data structures and dynamical initializable arrays. In Proceedings of the 43rd International Symposium on Mathematical Foundations of Computer Science (MFCS 2018), volume 117 of LIPIcs, pages 65:1–65:16. Schloss Dagstuhl - Leibniz-Zentrum f¨urInformatik, 2018. doi:10.4230/LIPIcs.MFCS.2018.65.

[9] Donald E. Knuth. Simpath, 2008. Last accessed 11/3/2017. URL: https:// www-cs-faculty.stanford.edu/~knuth/programs/simpath.w. [10] Donald E. Knuth. The Art of Computer Programming: Bitwise Tricks & Techniques; Binary Decision Diagrams. Addison-Wesley, 2009.

[11] Jacob Teo Por Loong, Jelani Nelson, and Huacheng Yu. Fillable arrays with constant time operations and a single bit of redundancy. CoRR, abs/1709.09574, 2017. arXiv: 1709.09574.

[12] Kurt Mehlhorn. Data Structures and Algorithms 1: Sorting and Searching, volume 1 of EATCS Monographs on Theoretical Computer Science. Springer, 1984. doi:10.1007/ 978-3-642-69672-5.

[13] Shin-ichi Minato. Zero-suppressed bdds for set manipulation in combinatorial problems. In Proceedings of the 30th Design Automation Conference, pages 272–277, 1993. doi: 10.1145/157485.164890.

[14] Shin-ichi Minato. Power of enumeration - recent topics on bdd/zdd-based techniques for discrete structure manipulation. IEICE Transactions, 100-D(8):1556–1562, 2017.

[15] Gonzalo Navarro. Constant-time array initialization in little space. Manuscript, 2012. URL: http://www.dcc.uchile.cl/~gnavarro/ps/sccc12.pdf. [16] Gonzalo Navarro. Spaces, trees, and colors: The algorithmic landscape of document re- trieval on sequences. ACM Computing Surveys, 46(4):52:1–52:47, 2014. doi:10.1145/ 2535933.

9 [17] Tsutomu Sasao and Jon T. Butler. Applications of Zero-Suppressed Decision Diagrams. Synthesis Lectures on Digital Circuits and Systems. Morgan & Claypool Publishers, 2014. doi:10.2200/S00612ED1V01Y201411DCS045.

10 Appendix

Algorithm 2: init(v) and read(i) 1 Function init(v): 2 b ← 0 ; 3 initv ← v ;

4 Function read(i): 0 5 i ← bi/2c // Bi0 is the block that contains A[i]. 6 Bk ← chainedTo(Bi0 ); 7 if i < 2b then 8 if Bk 6= None then 9 return initv ; 10 else 11 return A[i]; 12 else 13 if Bk 6= None then 14 if i mod 2 = 0 then 15 return A[A[i] + 1] ; 16 else 17 return A[i]; 18 else 19 return initv ;

11 Algorithm 3: Tools

1 Function chainedTo(Bi): 2 k0 ← A[2i]; 0 0 3 k ← bk /2c // Bk is the block that contains A[k ]. 4 if k0 mod 2 = 0 and A[k0] = 2i and (i < b ≤ k or k < b ≤ i) then 5 return Bk ; 6 else 7 return None ;

8 Function makeChain(Bi,Bj): 9 A[2i] ← 2j ; 10 A[2j] ← 2i ;

11 Function breakChain(Bi): 12 Bk ← chainedTo(Bi); 13 if Bk 6= None then 14 A[2k] = 2k ;

15 Function initBlock(Bi): 16 A[2i] ← initv ; 17 A[2i + 1] ← initv ;

18 Function extend(): 19 Bk ← chainedTo(Bb); 20 b ← b + 1 ; 21 if Bk = None then 22 k ← b − 1 ; 23 else 24 Bb−1 ← (A[2k + 1], A[2b − 1]) ; 25 breakChain(Bb−1); 26 initBlock(Bk); 27 breakChain(Bk); // Bk is an unwritten block in the unwritten chained area which is not chained yet. 28 return Bk ;

12