Algorithm tutorials
Rolling Hash (Rabin-Karp)
Polynomial string hashing with prefix hashes gives the hash of any substring in O(1), which turns substring equality into integer comparison: Rabin-Karp matching, longest duplicate substring, palindrome checks and more.
Intuition
Comparing two substrings character by character costs their length. Hashing compresses each string into one integer, so equal strings always get equal numbers and different strings almost always get different ones. Compare numbers instead of strings and the cost drops to O(1).
The standard choice is a polynomial hash: read the string as a number in base B, modulo a prime M.
hash(s) = (s[0] * B^(k-1) + s[1] * B^(k-2) + ... + s[k-1]) mod M
Two properties make it "rolling":
- Prefix hashes.
h[i + 1] = h[i] * B + s[i], so all prefix hashes take one pass. - Any substring in O(1). Just like a prefix sum:
hash(s[l:r]) = h[r] - h[l] * B^(r - l)(modM). Sliding a fixed-length window one step costsO(1)too.
Different strings can still collide. For two fixed different strings of length k and a random base, the hash difference is a nonzero polynomial in B of degree below k, so a collision happens with probability at most k / M. With M = 2^61 - 1 that is negligible even over billions of comparisons, which is why that modulus is the safe default.
When to use it in interviews / OAs
- Substring equality asked many times: "are these two substrings equal", "is
s[l:r]a palindrome" (compare with the reversed string's hash). - Pattern matching when you want something shorter than KMP (Rabin-Karp).
- "Longest substring that appears twice", "longest common substring of two strings": binary search on the length plus a hash set per length.
- Counting distinct substrings of a given length, repeated DNA sequences, distinct "echo" substrings.
- Comparing whole objects cheaply: serialized subtrees, rows of a grid, paths.
Template
Prefix hashes
import random
MOD = (1 << 61) - 1 # Mersenne prime
BASE = random.randrange(10**6, MOD - 1) # random base: no fixed input can be built to collide
class StringHash:
def __init__(self, s: str):
n = len(s)
self.h = [0] * (n + 1) # h[i] = hash of s[:i]
self.p = [1] * (n + 1) # p[i] = BASE**i % MOD
for i, ch in enumerate(s):
self.h[i + 1] = (self.h[i] * BASE + ord(ch) + 1) % MOD # +1: no character maps to 0
self.p[i + 1] = self.p[i] * BASE % MOD
def get(self, l: int, r: int) -> int:
# hash of s[l:r] in O(1)
return (self.h[r] - self.h[l] * self.p[r - l]) % MOD
Rabin-Karp matching
def rabin_karp(text: str, pattern: str) -> list[int]:
m = len(pattern)
if m > len(text):
return []
ht, target = StringHash(text), StringHash(pattern).get(0, m)
return [i for i in range(len(text) - m + 1) if ht.get(i, i + m) == target]
If a wrong answer is unacceptable, confirm each hit with text[i:i + m] == pattern; that adds O(m) per reported match.
Longest duplicate substring: binary search on the length
If some substring of length L occurs twice, so does one of every shorter length (take its prefix). That monotonicity allows binary search; each check hashes all windows of one length.
def longest_duplicate_substring(s: str) -> str:
sh = StringHash(s)
def find_dup(length: int) -> int: # start of a repeated window, or -1
seen = set()
for i in range(len(s) - length + 1):
x = sh.get(i, i + length)
if x in seen:
return i
seen.add(x)
return -1
lo, hi = 1, len(s) - 1
best, start = 0, 0
while lo <= hi:
mid = (lo + hi) // 2
i = find_dup(mid)
if i != -1:
best, start = mid, i
lo = mid + 1
else:
hi = mid - 1
return s[start:start + best]
Palindrome test for any substring
s[l:r] is a palindrome iff it equals its reverse, i.e. the matching block of reversed(s).
def make_palindrome_checker(s: str):
fwd, rev, n = StringHash(s), StringHash(s[::-1]), len(s)
def is_pal(l: int, r: int) -> bool: # is s[l:r] a palindrome?
return fwd.get(l, r) == rev.get(n - r, n - l)
return is_pal
Complexity
Building prefix hashes is O(n) time and memory; each substring hash is O(1). Rabin-Karp is O(n + m) expected. The longest duplicate substring costs O(n log n) expected (log of the length range, O(n) set operations per step). Python's big integers make % MOD products safe; in C++ use __int128 or the Mersenne-specific reduction for the 61-bit modulus.
Common pitfalls
- A character mapped to 0. With
'a' -> 0,"a","aa"and"aaa"all hash to 0. Map characters to values from 1 up. - Small modulus plus many comparisons. With
M = 10^9 + 7, putting10^5hashes into one set already expects a collision (birthday bound: aboutn^2 / (2M)pairs collide). Use2^61 - 1or two independent moduli. - A fixed, well-known base and modulus can be attacked by crafted inputs in contests and some OAs; randomize the base at runtime. The
2^64natural-overflow hash has known adversarial strings. - Negative values in languages where
%can return negatives: addMbefore taking the remainder. - Different substring formulas for left-to-right vs right-to-left polynomials. Pick one convention and derive
getfor it. - Hashing slices.
set(s[i:i + L] for i in ...)orhash(s[i:j])copies and rehashesLcharacters each time, which quietly throws away theO(1)advantage; useget(l, r).
Interview variations
- Repeated DNA sequences. Fixed window length 10: slide the window and keep a set of hashes (or of the substrings themselves, since they are short).
- Longest duplicate substring / longest common substring. Binary search on length; for two strings, collect window hashes of one and probe with the other.
- Distinct echo substrings. Count substrings of the form
aaby comparingget(i, i + L)withget(i + L, i + 2L). - Longest common prefix of two suffixes in
O(log n): binary search on the length with hash comparisons. It also compares two substrings lexicographically (look at the first differing character). - Matching with at most k mismatches. Jump over equal stretches with the LCP binary search, at most
k + 1times per window. - Hashing structures. Serialize subtrees (find duplicate subtrees) or rows, then compare hashes.