Suffix Structures & Rolling Hash
Work through examples, try the reasoning yourself, then put it into practice.
View SlidesLesson
4 min readSubstring fingerprints and suffix structures
The exercise answers many inclusive, byte-indexed substring queries on one unchanging string. Comparing two length-k slices directly takes O(k) in the worst case. Prefix fingerprints reuse work across queries, at the price of a possible hash collision. A matching fingerprint is evidence, not a proof.
Derive the prefix recurrence
Choose base B and modulus M. Encode each byte as byte+1, so values lie in
1..256, and define P[0]=0, W[0]=1:
P[i+1] = (P[i] * B + value(s[i])) mod M
W[i+1] = (W[i] * B) mod M
After three bytes, the unreduced polynomial is v0*B² + v1*B + v2.
Multiplying the old prefix by B shifts every exponent up one; adding the new
byte supplies the constant term. The arrays each need n+1 entries because
P[i] represents the half-open prefix s[0:i].
For inclusive endpoints l and r, let k=r-l+1. Expanding P[r+1] separates it
into the first l bytes shifted k positions and the wanted final k bytes:
P[r+1] = P[l] * B^k + fingerprint(s[l:r+1]) (mod M)
H(l,r) = (P[r+1] - P[l] * W[k]) mod M
The multiplication is necessary. Subtracting P[l] without shifting removes
the wrong powers. Because Go's remainder may be negative, reduce the product,
subtract, then add M once if negative; both reduced operands lie in [0,M).
Compare lengths before hashes: this formula alone does not encode the length.
Work an example by hand
For easy arithmetic only, encode A=1, B=2, C=3; take B=5 and M=101. For ABAB,
the prefixes are [0,1,7,36,81], and powers begin [1,5,25,24,19].
The first AB, [0,1], hashes to 7. The second AB, [2,3], hashes to
(81 - 7*25) mod 101 = -94 mod 101 = 7. Their positions differ, but extraction
normalizes both substrings to the same exponents.
Try [1,2]: P[3]-P[1]*25 = 36-25 = 11, matching BA's 2*5+1.
The exercise uses the actual bytes plus one and much larger constants; do not
copy these teaching parameters into its implementation.
Know what a collision means
Under the same toy parameters, AAA gives 31 while BBA gives 61; those differ.
But with base 5, modulus 7, AA hashes to 6 and BC hashes to
2*5+3=13 mod 7=6. They are unequal strings with equal length and equal hash.
This is not an overflow bug. Mapping many strings to a finite set forces
collisions.
Use two prefix/power pairs and require both fingerprints to agree. This greatly reduces accidental matches for ordinary inputs, but fixed public parameters do not supply an unconditional random probability bound against chosen inputs. The reference uses base 911382323 and moduli 1000000007 and 1000000009. Products of reduced values fit signed int64; an arbitrary larger modulus might not. Preprocessing uses O(n) time and space; fingerprint queries use O(1) time.
The supplied implementation additionally compares the original byte slices when both hashes agree. Therefore its returned equality is exact. If V is the sum of lengths of those verified candidates, total time is O(n+q+V), with O(n) auxiliary space. Repeated long equal substrings can make V large. Removing that comparison gives O(n+q) time and makes the result probabilistic; the exercise deliberately requires exact answers and states this tradeoff.
When to choose a suffix structure instead
A suffix array stores starting indices of suffixes in lexical order. For
banana, those indices are [5,3,1,0,4,2]: a, ana, anana, banana, na,
nana. Adjacent LCP lengths are [1,3,0,0,2]. The longest repeated substring
has length 3; it is ana and may overlap itself.
There are n(n+1)/2=21 nonempty substrings counted by positions. Subtract the
sum of adjacent LCP values, 6, to get 15 distinct substring values: each sorted
suffix contributes all its prefixes except those already shared with its
predecessor. A suffix array plus LCP and range-minimum queries can support
exact substring equality without scanning every successful match, but building
that machinery is outside this exercise. A suffix automaton also represents
all substrings; its usual linear construction assumes constant-time transitions
for a fixed alphabet. Neither structure is automatically implemented by a
rolling hash.
Check your reconstruction
- Why does
[l,r]useP[r+1]? Answer: the prefix array counts bytes, while r is an inclusive index. - Can double hashes prove two arbitrary strings equal? Answer: no; verify bytes or use an exact structure when false matches are unacceptable.
- Is the second byte of a UTF-8 character a valid byte index? Answer: yes for this API; byte slices need not end on Unicode character boundaries.
- An empty string receives two malformed inclusive queries. Answer: two false results; output length always equals query count.
Princeton's Rabin–Karp implementation shows both rolling arithmetic and the verification step. Its suffix-array implementation provides a separate exact ordering approach.
Practice sequence
Use the order below for a guided path, or choose an item to revisit. A pass records a successful attempt; try a fresh example later to check what you can reconstruct.
- Step 1
Substring Equality Queries
- Step 2
Suffix Structures Checkpoint
Work fresh traces, diagnose invalid implementations, and identify model boundaries.