DSA DSA In Depth
0%
lesson rabin-karprolling-hashstring-matching

Rabin-Karp Rolling Hash: Mersenne Prime Moduli (2^61 - 1) & Birthday Collision Avoidance

7 min read Module 4.1: Tries, Exact String Matching & Multi-Pattern Automata Updated Oct 23, 2026
📄 Cheat Sheet
Rabin-Karp Algorithm & Polynomial Rolling Hash
Abdul Bari Rabin-Karp Algorithm & Polynomial Rolling Hash Watch on YouTube ↗

Explains the polynomial view of strings and how sliding a window updates the hash in O(1) arithmetic operations.

0:00 / --:--

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!

Schwartz-Zippel Lemma & Birthday Paradox Bound:
  • 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}$.
O(1) Substring Hash: hash(L..R) = (P[R+1] - P[L] * B^(R-L+1)) mod (2^61 - 1) P[R+1]: S[0..L-1] * B^(R-L+1) + S[L..R] (Target Hash!) P[L]*B^k: S[0..L-1] * B^(R-L+1) <-- Exact cancellation via subtraction mod M! Division-Free Mod 2^61 - 1: Because 2^61 ≡ 1 (mod 2^61 - 1), for any 122-bit product x: modMersenne(x) = (x & ((1n << 61n) - 1n)) + (x >> 61n); if (res >= M) res -= M; Replaces 40-cycle hardware DIV instruction with 1-cycle bit shift + mask + add!
flowchart LR LCP["lcp(i, j): Binary Search len in [0 .. minLen]"] --> Mid["mid = (lo + hi + 1) >> 1"] Mid --> Eq{"hash(i, i+mid-1) == hash(j, j+mid-1)?"} Eq -- Yes --> GoHi["lo = mid (prefix matches!)"] Eq -- No --> GoLo["hi = mid - 1 (mismatch inside)"] GoHi & GoLo --> Lex["Compare S[i + lo] vs S[j + lo] for O(log N) Substring Lexicographical Compare!"]
Hardware & Cache Reality — Why $M = 2^{61} - 1$ Avoids Hardware 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.
Rabin-Karp Polynomial Rolling Hash Ring & Boyer-Moore-Horspool / Teddy SIMD Skip
O(1) Rolling Hash · O(N/M) Horspool Skip

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.

Step 1 / 10
Custom Input:
Timeline

Invariant & State

Rolling Hash Recurrence: H₀ = (3·10² + 1·10¹ + 4) mod 97 = 23 (Target H("159") = 159 mod 97 = 62)
🌳 Live 2D Hierarchical Tree Topology — Rabin-Karp Polynomial Rolling Hash Ring & Boyer-Moore-Horspool / Teddy SIMD Skip Visited/Active Pivot/Root Resolved
evict ne 3 [0] 1 [1] H=314 4 [2] 1 [3] 5 [4] 9 [5] 2 [6]
[0]3evict next
[1]1
[2]4H=314
[3]1
[4]5
[5]9
[6]2
Target Pattern P
"159" (H(P) = 62)
Active Window
T[0..2] = "314"
Window Hash H_i
314 mod 97 = 23
Update Cost
O(1) Modular Ops
[Step 1] Initial window T[0..2] = "314": polynomial hash H₀ = 314 mod 97 = 23 != H("159") = 62.

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)
Common Pitfall — Mapping the First Character to 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))$.
Check yourself Question 1 of 4
Concept Check Birthday Paradox Collision Threshold for Single 32-Bit Hash recall

A developer uses a single polynomial hash modulo $M = 10^9 + 7 \approx 2^{30}$ to deduplicate all substrings of length $L$ in a text of length $N = 10^5$ without verifying matching substrings character-by-character.

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

Why do $T_{11}$ and $T_{11}'$ (of length $2^{11} = 2048$) collide for EVERY possible odd base $B$ modulo $2^{64}$?

Key Takeaways

Precomputing prefix polynomial hashes P[i] = (P[i-1]*B + S[i-1]) mod M allows O(1) hash extraction of any substring S[L..R] via (P[R+1] - P[L]*B^(R-L+1)) mod M.
By the Birthday Paradox, comparing K = 10^5 substrings requires a modulus M >> K^2; a single 32-bit prime (10^9 + 7) collides with >99% probability.
The Mersenne prime M = 2^61 - 1 with a runtime-randomized base supports division-free reduction (x & M) + (x >> 61) and defeats both Birthday collisions and Thue-Morse anti-hash strings.
Combining O(1) substring hashing with binary search computes the Longest Common Prefix (LCP) and lexicographical order of any two substrings in O(log N) time.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue