1. Polynomial Prefix Hashing & $O(1)$ Substring Extraction
Define the 1-indexed prefix hash array $P[0..N]$ with $P[0] = 0$ and recurrence $P[i] = (P[i-1] \cdot B + S[i-1]) \bmod M$, alongside power table $\text{powB}[k] = B^k \bmod M$. Notice that $P[R+1]$ contains $P[L]$ shifted left by $R - L + 1$ powers of $B$, plus the polynomial of $S[L..R]$:
$$P[R+1] = P[L] \cdot B^{R - L + 1} + \sum_{j=L}^{R} S[j] \cdot B^{R - j} \pmod M$$
Subtracting $P[L] \cdot \text{powB}[R - L + 1]$ modulo $M$ isolates the exact canonical hash of $S[L..R]$ in $O(1)$ time!
- Single Pair Comparison: Two distinct strings of length $L$ define a non-zero difference polynomial of degree $< L$. By the Schwartz-Zippel Lemma (Lagrange's Theorem), if base $B$ is chosen uniformly at random from $[1, M-1]$, the probability that $H(S_1) \equiv H(S_2) \pmod M$ is at most $\frac{L - 1}{M - 1}$.
- All-Pairs / Set of $K$ Substrings (Birthday Paradox): When inserting $K$ substring hashes of length $L$ into a hash set (or sorting them), there are $\binom{K}{2} \approx \frac{K^2}{2}$ pairs. By the union bound, the failure probability is bounded by $\frac{K^2 \cdot L}{2M}$. With $M = 10^9 + 7$ and $K = 10^5$, $\frac{K^2}{2M} \approx 5 > 1$ (guaranteed collision!). With $M = 2^{61} - 1 \approx 2.3 \times 10^{18}$ (or a Double Hash $(M_1, M_2)$), $\frac{K^2}{2M} \approx 2.1 \times 10^{-9}$.
DIV Bottlenecks and Anti-Hash Tests: Integer division (idiv on x86-64) has a latency of 25–45 CPU clock cycles and cannot be pipelined well. For the Mersenne prime $M = 2^{61} - 1$, $2^{61} \equiv 1 \pmod M$, so any product $x = \text{hi} \cdot 2^{61} + \text{lo}$ satisfies $x \equiv \text{hi} + \text{lo} \pmod M$—computable using only bit shifts, bitwise AND, and addition! Furthermore, never use unsigned int64 overflow ($M = 2^{64}$) with a fixed base: the Thue-Morse sequence ($T_0 = \text{'a'}, T_1 = \text{'ab'}, T_2 = \text{'abba'}, \dots$) has degree-$12$ root collisions at $2^{64}$ for every odd base $B$! Choosing $M = 2^{61} - 1$ with a runtime-randomized base $B$ is immune to Thue-Morse anti-hash tests.
Trace Rabin-Karp O(1) modular window slides H_{i+1} = ((H_i − T[i]·B^{m−1})·B + T[i+m]) mod P, Boyer-Moore-Horspool right-to-left bad-character skips, and ripgrep Teddy 16-byte SIMD nybble-mask filtering.
Invariant & State
2. Production Implementation: Mersenne $2^{61}-1$ Rolling Hash with $O(1)$ Substring Hash & $O(\log N)$ LCP
const MOD61 = (1n << 61n) - 1n;
// Random odd base chosen per process startup in [257 .. 10^9] to defeat adversarial inputs
const RANDOM_BASE = BigInt(257 + Math.floor(Math.random() * 1000000000));
function mod61(x) {
let res = (x & MOD61) + (x >> 61n);
if (res >= MOD61) res -= MOD61;
return res;
}
function mul61(a, b) {
return mod61(a * b);
}
/**
* RollingHash61: O(N) build, O(1) arbitrary substring hash, O(log N) substring LCP & compare
*/
class RollingHash61 {
constructor(str, base = RANDOM_BASE) {
this.str = str;
this.n = str.length;
this.pref = new BigUint64Array(this.n + 1);
this.pow = new BigUint64Array(this.n + 1);
this.pow[0] = 1n;
for (let i = 0; i < this.n; i++) {
const code = BigInt(str.charCodeAt(i));
this.pref[i + 1] = mod61(this.pref[i] * base + code);
this.pow[i + 1] = mul61(this.pow[i], base);
}
}
// O(1) hash of closed substring str[l..r] (0-indexed); returns 0n for empty range l > r
getHash(l, r) {
if (l > r) return 0n;
const sub = this.pref[r + 1] + MOD61 - mul61(this.pref[l], this.pow[r - l + 1]);
return sub >= MOD61 ? sub - MOD61 : sub;
}
// O(log N) Longest Common Prefix of suffixes str[i..] and str[j..]
lcp(i, j) {
let lo = 0;
let hi = this.n - Math.max(i, j);
while (lo < hi) {
const mid = (lo + hi + 1) >> 1;
if (this.getHash(i, i + mid - 1) === this.getHash(j, j + mid - 1)) {
lo = mid;
} else {
hi = mid - 1;
}
}
return lo;
}
// O(log N) lexicographical comparison of substrings str[l1..r1] and str[l2..r2]
compareSubstrings(l1, r1, l2, r2) {
const len1 = r1 - l1 + 1, len2 = r2 - l2 + 1;
const common = Math.min(len1, len2, this.lcp(l1, l2));
if (common >= Math.min(len1, len2)) return len1 - len2;
return this.str.charCodeAt(l1 + common) - this.str.charCodeAt(l2 + common);
}
}
3. Complexity Matrix
| Operation / Modulus Choice | Preprocessing | Operation Time | Collision Risk for $K = 10^5$ Substrings |
|---|---|---|---|
| Single 32-bit Prime ($M = 10^9 + 7$) | $\Theta(N)$ | $O(1)$ substring hash | $\approx 99.3\%$ (Broken by Birthday Paradox!) |
| Unsigned 64-bit Overflow ($M = 2^{64}$, fixed base) | $\Theta(N)$ | $O(1)$ substring hash | $100\%$ on Thue-Morse string of length $2048$ |
| Mersenne Prime $M = 2^{61} - 1$ + Random Base $B$ | $\Theta(N)$ | $O(1)$ hash, $O(\log N)$ LCP | $\le \frac{K^2 L}{2(2^{61}-1)} \approx 2 \times 10^{-9}$ |
| Rabin-Karp Pattern Search ($|T| = N, |P| = M$) | $\Theta(N + M)$ | $\Theta(N + M)$ expected | $0$ (if verified on hash hit) |
0 ('a' -> 0): Never map 'a' to 0 (str.charCodeAt(i) - 97) in a polynomial rolling hash! If 'a' maps to 0, then "a", "aa", "aaa", and "" all evaluate to the exact same polynomial hash 0! Always map characters to strictly positive integers $\ge 1$ (str.charCodeAt(i) - 96 or raw UTF-16 str.charCodeAt(i)), and choose base $B > \max_c(\text{charCode}(c))$.