Fingerprinting and String Matching
The question
Here is a text of sixteen digits and a pattern of four. Where does the pattern occur?
The answer is at positions 3 and 12 (counting from 0). Now scale it up. Two data centres each hold a copy of a 1 GB file, and one copy may have been corrupted. Are they still equal? Or a text of a hundred thousand characters and a pattern of fifty: where does the pattern occur?
Both questions compare big objects, and the obvious way reads all of both. This module replaces each object by a fingerprint, its remainder modulo a prime chosen at random. Equal objects always have equal fingerprints. Different ones almost never do, and we can say exactly how rarely. Then we slide the fingerprint along a text in constant time per step, which is the Karp–Rabin algorithm.
The naive approach, and where it wastes work
To find a pattern of length in a text of length , try each of the starting positions and compare characters until one differs. On most texts the first comparison fails and this is fast. On the worst texts it isn't:
def naive_comparisons(text, pat):
"""Character comparisons made by the obvious scan, and the positions it finds."""
count, found, m = 0, [], len(pat)
for i in range(len(text) - m + 1):
for j in range(m):
count += 1
if text[i + j] != pat[j]:
break
else:
found.append(i)
return count, found
TEXT, PAT = "6328465307928465", "8465"
assert naive_comparisons(TEXT, PAT)[1] == [3, 12]
n = 10_000
assert naive_comparisons("a" * n, "a" * 49 + "b")[0] == 50 * (n - 49)
On the text aaa…a with the pattern a…ab (49 a's, then b), every position matches 49
characters before failing on the 50th: comparisons, . The waste is
exact. Moving one step right, the scan throws away 49 characters it has just read and reads 48
of them again.
For the two files it is worse: comparing them means sending one of them across, bits.
The model: coins after the input
We count word operations: arithmetic on numbers that fit in a machine word (below here, for the prime range and the base defined below) costs one step, and so does reading or comparing one character.
The algorithms here flip coins, and the guarantee is about those coins. The input is fixed first, by anyone, even an adversary. Then the algorithm picks its prime. Every probability below is over that choice, for every input. This is the same setting as universal hashing in module 04: nothing is assumed about the data.
Comparing remainders
Alice holds an -bit number (her file, read as one big binary number), and Bob holds . Alice picks a random prime and sends the pair . Bob answers "equal" if is the same number.
If , Bob is always right. If , Bob is wrong exactly when , that is, when divides . So the question becomes: how many primes can divide one fixed nonzero number?
Lemma. A number has fewer than distinct prime factors. Proof: if are distinct primes dividing , their product divides , and each is at least 2, so , and .
Theorem (fingerprint equality)
For fixed -bit numbers and a prime chosen uniformly from the primes up to : .
The first equality is exact, and on small numbers you can count it:
import math
def primes_upto(T):
sieve = bytearray([1]) * (T + 1)
sieve[0] = sieve[1] = 0
for q in range(2, math.isqrt(T) + 1):
if sieve[q]:
sieve[q * q::q] = bytearray(len(sieve[q * q::q]))
return [q for q in range(T + 1) if sieve[q]]
x, y = 9_876_543, 9_366_033
assert x - y == 510_510 == 2 * 3 * 5 * 7 * 11 * 13 * 17
assert x < 2**24 and y < 2**24
fooled_100 = [q for q in primes_upto(100) if x % q == y % q]
assert fooled_100 == [2, 3, 5, 7, 11, 13, 17] and len(primes_upto(100)) == 25
fooled_1000 = [q for q in primes_upto(1000) if x % q == y % q]
assert len(fooled_1000) == 7 and len(primes_upto(1000)) == 168
These two 24-bit numbers differ by , the product of the first seven primes, which is about as unlucky as a difference can be. With , 7 of the 25 primes fool Bob (error ). With there are still only those 7 bad primes, but now among 168 (about ). The bad primes are fixed by the input; raising adds only good ones.
Predict: why not skip the coin and use one huge prime, say , every time?
Because the input can be chosen after the prime is known. Whoever writes (or any ) fools Bob on every run. The theorem says "for fixed , over a random ". A prime fixed in advance has no probability left to argue with.
How many primes are there?
The bound is only useful if we know , the number of primes up to . The prime number theorem (stated, not proved here) says : about one number in is prime. For a proof we need an inequality that holds at every , not just in the limit. Rosser and Schoenfeld proved one (stated here):
flags = bytearray(20_001)
for q in primes_upto(20_000):
flags[q] = 1
count = 0
for T in range(2, 20_001):
count += flags[T] # count == pi(T)
assert T < 17 or count > T / math.log(T)
table = {T: (len(primes_upto(T)), T / math.log(T)) for T in (100, 1000, 10**4, 10**6)}
assert table[100][0] == 25 and table[1000][0] == 168
assert table[10**4][0] == 1229 and table[10**6][0] == 78498
assert round(table[10**6][1]) == 72382
| 100 | 25 | 21.7 |
| 1,000 | 168 | 144.8 |
| 10,000 | 1,229 | 1,085.7 |
| 1,000,000 | 78,498 | 72,382.4 |
Primes are plentiful, and that has a second use: a random prime is cheap to draw. Pick a uniform integer in and keep it if it is prime. Each draw succeeds with probability , so the expected number of draws is , which is less than by the inequality. For that is draws. Rejection also keeps the prime uniform over the primes up to , which is exactly what the theorem assumes.
Now choose . With the inequality gives , so the error is below . For the 1 GB files, bits:
nbits = 8 * 10**9
assert math.ceil(math.log2(nbits**2)) == 66 # p and x mod p fit in 66 bits each
error = 2 * math.log(nbits) / nbits
assert 5.6e-9 < error < 5.8e-9
Alice sends 132 bits instead of eight billion, and Bob is fooled with probability below , whatever the files are. If that is not small enough, repeat with fresh primes: independent rounds multiply the error to below .
Rolling the fingerprint along a text
Back to the digits. Read each window of digits as a number and the pattern as . The pattern occurs at exactly when , so we could compare with . Computing each from scratch costs steps per window, which is no better than the naive scan. The point of Karp–Rabin is that the next window's value follows from the previous one. Drop the leading digit , shift, and bring in :
in base for digits. Going from 6328 to 3284 is . This is
exact arithmetic, and reduction mod respects , and , so the same identity holds
for the remainders once is precomputed. That makes it two multiplications, an
addition and a subtraction per step, whatever is. With , :
p = 13
fp = [int(TEXT[i:i + 4]) % p for i in range(13)] # from scratch, to check against
assert int(PAT) % p == 2 and pow(10, 3, p) == 12
assert fp == [10, 8, 12, 2, 12, 4, 3, 11, 12, 11, 2, 12, 2]
for i in range(12): # the roll gives the same numbers
assert ((fp[i] - int(TEXT[i]) * 12) * 10 + int(TEXT[i + 4])) % p == fp[i + 1]
The first step, by hand: , and indeed .
Invariant
After step , the rolling value equals , the remainder of the window that starts at . So window is reported as a candidate exactly when divides .
The invariant holds at because the first window is computed directly, and each roll preserves it by the identity above. By induction it holds for every window.
The trace, and a false alarm
All thirteen residues are 10 8 12 2 12 4 3 11 12 11 2 12 2. Three windows share the pattern's
residue 2: positions 3, 10 and 12.
Predict: which primes other than 13 would also have raised the alarm at position 10?
Exactly the primes that divide : the primes 3, 7
and 13. Every other prime tells 9284 and 8465 apart. That is the whole theorem in one
window: the bad primes are the prime factors of a fixed difference.
assert TEXT[10:14] == "9284" and 9284 - 8465 == 819 == 3 * 3 * 7 * 13
assert [q for q in primes_upto(50) if 819 % q == 0] == [3, 7, 13]
How many false alarms?
A window is a false positive when divides . Both are -digit numbers in base , so , and by the lemma this difference has fewer than prime factors. So
The false positives in a text are not independent of each other, since one prime decides all of them. That doesn't matter. Linearity of expectation needs no independence, so the expected number of false positives is the sum over windows:
On our text we can compute the expectation exactly. Draw uniformly from the 15 primes up to 50 and count, for each of the 11 non-matching windows, how many of those primes divide its difference from 8465:
from fractions import Fraction
small = primes_upto(50)
bad = sum(sum(1 for q in small if (int(TEXT[i:i + 4]) - 8465) % q == 0)
for i in range(13) if TEXT[i:i + 4] != PAT)
assert len(small) == 15 and bad == 14
assert Fraction(bad, 15) == Fraction(14, 15)
# a difference below 10**4 has at most 5 distinct prime factors: 2*3*5*7*11 = 2310 < 10**4 < 30030
assert math.prod(primes_upto(11)) == 2310 and math.prod(primes_upto(13)) == 30030
The exact expectation is false positives. The bound, which charges every one of the 11 windows the most prime factors a difference below can have (five), gives . It is loose, but it holds for every text, and it is what tells us how to pick : with , and , gives fewer than 0.83 expected false positives in the whole text.
bound = 10**5 * 50 * 8 / (10**9 / math.log(10**9)) # pi(10**9) > 10**9 / ln(10**9)
assert 0.82 < bound < 0.84
assert 50 * 8 == 400 and len(primes_upto(1000)) == 168 # T = 1000 is useless for m = 50, B = 256
Monte Carlo or Las Vegas
A candidate is not yet a match. There are two honest things to do with it.
- Monte Carlo: report every candidate. The time is always , and the answer is wrong with probability at most the bound above. On our text with it reports 3, 10 and 12.
- Las Vegas: compare each candidate with the pattern character by character, and report it only if they agree. The answer is never wrong, whatever the prime. What is random now is the time: each false positive costs up to extra comparisons. With it reports 3 and 12.
Checking turns a small probability of a wrong answer into a small expected cost. The error is one-sided in both versions: a true occurrence always has the pattern's residue, so neither version ever misses one.
Predict: with the check in place, is a small prime range like still safe?
It is still correct, since every reported match was compared character by character. It is no longer fast. With only 8 primes to choose from, false candidates are common, and each one costs a comparison of up to characters. The prime range buys speed, and the check buys correctness.
Complexity
In the word model:
- Equality of two -bit files: bits sent, one-sided error below ; with , 132 bits and error below for 1 GB.
- Karp–Rabin, Monte Carlo: to fingerprint the pattern and the first window, per roll, so word operations on every input, with error probability below .
- Karp–Rabin, Las Vegas: always correct, in expected time , where occ is the number of true occurrences. The expectation is over the prime, on every input.
The occurrences term is real. On the text aaa…a with the pattern aaaa every window is a true
occurrence, and checking them all costs . The Knuth–Morris–Pratt algorithm finds all
occurrences deterministically in even then. No exact matcher can beat ,
because a character it never reads could complete an occurrence. Where fingerprints win is in
comparing many strings or substrings at once, where one pattern-specific table is not
enough.
Measure the claim
On aaa…a with the pattern of 49 a's and a b, every window differs from the pattern by exactly 1
in the last digit, and 1 has no prime factors. So Karp–Rabin never raises a false alarm here, and
its cost is one roll per window. The naive scan pays 50 per window:
for n in (10**3, 10**4, 10**5):
windows = n - 49
naive = 50 * windows # counted exactly above, at n = 10,000
assert abs(ord("a") - ord("b")) == 1 # |W_i - P| = 1: no prime divides it
print(n, round(naive / n, 2), round(windows / n, 3))
assert naive_comparisons("a" * 1000, "a" * 49 + "b")[0] == 50 * (1000 - 49)
| naive comparisons per character | Karp–Rabin rolls per character | |
|---|---|---|
| 1,000 | 47.55 | 0.951 |
| 10,000 | 49.76 | 0.995 |
| 100,000 | 49.98 | 1.000 |
The naive ratio climbs towards and Karp–Rabin's stays at 1, the against gap. On a random text, where the naive scan usually fails at the first character, both would be cheap. The worst case is where the proof earns its keep.
A problem that looks different
A backup service keeps yesterday's disk image. Today's image holds mostly the same data, but with bytes inserted and deleted at unknown places, so nothing lines up. Which 4 KB stretches of today's image already exist somewhere in yesterday's, at any byte offset? Sending them again wastes bandwidth. Nothing in the question mentions patterns or primes. The lab's last problem is a different one.
Practise
The lab starts with the rolling fingerprint, drawn one window at a time, with a check that it really rolls and doesn't recompute. It then traces the candidates on a new text and separates the true matches from a false alarm. After that you write a matcher that draws its own random prime and works with or without the check, measure what it reads per character against the naive scan, and count false alarms exactly. The last problem doesn't say what it is.
Recap
- You can now: bound the error of comparing remainders by (prime factors of the difference) / (primes up to ), and pick with the prime number theorem; roll a window's residue in constant time; trace Karp–Rabin by hand, false positive included; and turn a Monte Carlo matcher into a Las Vegas one by checking each candidate.
- Invariant: the rolling value is the current window's remainder, so a window is a candidate exactly when the random prime divides its difference from the pattern, and a fixed nonzero difference has fewer prime factors than it has bits.
- Complexity achieved: word operations with error below , or never wrong in expected for a large enough , against for the naive scan; file equality in bits instead of .
- Failure mode: a prime fixed before the input is known, or a too small for , which makes the bound say nothing. With and a difference can have up to 399 prime factors, and there are only 168 primes below 1000.
- In real software: rsync finds blocks that two versions of a file share with a weak checksum that it rolls one byte at a time, and confirms each candidate with a strong hash before trusting it. That is the same filter-then-check pattern as Las Vegas Karp–Rabin.
- Next: module 06 answers range queries with a tree whose every node already knows the answer for its block.
Check yourself
- Why does sending with a random prime detect with high probability, and why must the prime be chosen after the files are fixed?
- Keep the prime fixed but choose the base at random instead. Is Karp–Rabin still safe against a text chosen by an adversary?
- On the text
aaa…awith the patternaaaa, and on a random text over 26 letters, compare Las Vegas Karp–Rabin with the naive scan and with Knuth–Morris–Pratt. Which one wins where?
Topic list modeled on the public schedule of CMU 15-451/651 Algorithm Design and Analysis (Fall 2025). All lecture text, proofs, examples, code, and exercises are original to SciMigo; no CMU course materials are reproduced. This course is independent and not affiliated with or endorsed by Carnegie Mellon University.