String Algorithms contents
Prefix Function and Knuth-Morris-Pratt
Compute the longest proper border of every prefix in O(n), and use it for pattern matching, string periods and counting occurrences.
Read first: Strings
For a string of length , the prefix function is the length of the longest proper prefix of that is also a suffix of . "Proper" means it is not the whole substring itself. Prefix-suffix pairs like this are called borders.
For "abcabcd":
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | |
|---|---|---|---|---|---|---|---|
| a | b | c | a | b | c | d | |
| 0 | 0 | 0 | 1 | 2 | 3 | 0 |
For instance because "abc" is both a prefix and a suffix of "abcabc".
Computing it in
Compute from . Let be the length of the current border. If , the border extends: . If not, we fall back to the next shorter border, which is , and try again, until .
The total number of fall-backs is bounded by the total number of extensions (each fall-back strictly decreases , and grows by at most 1 per step), so the algorithm is linear.
def prefix_function(s):
n = len(s)
pi = [0] * n
for i in range(1, n):
k = pi[i - 1]
while k > 0 and s[i] != s[k]:
k = pi[k - 1]
if s[i] == s[k]:
k += 1
pi[i] = k
return pi
assert prefix_function("abcabcd") == [0, 0, 0, 1, 2, 3, 0]
assert prefix_function("aabaaab") == [0, 1, 0, 1, 2, 2, 3]
assert prefix_function("aaaa") == [0, 1, 2, 3]
assert prefix_function("") == []
def prefix_function_naive(s):
return [
max((k for k in range(i + 1) if s[:k] == s[i + 1 - k : i + 1]), default=0)
for i in range(len(s))
]
import random
random.seed(1)
for _ in range(300):
t = "".join(random.choice("ab") for _ in range(random.randint(0, 25)))
assert prefix_function(t) == prefix_function_naive(t)Pattern matching (KMP)
To find pattern in text , compute the prefix function of t + "#" + s, where # is a separator that occurs in neither string. Every position with marks an occurrence that ends at inside the combined string.
def kmp_search(s, t):
"""Start positions of every occurrence of t in s, in O(|s| + |t|)."""
if not t:
return list(range(len(s) + 1))
pi = prefix_function(t + "\0" + s) # "\0" is assumed not to occur
m = len(t)
return [i - 2 * m for i in range(2 * m, len(pi)) if pi[i] == m]
assert kmp_search("abracadabra", "abra") == [0, 7]
assert kmp_search("aaaaa", "aa") == [0, 1, 2, 3]
assert kmp_search("hello", "xyz") == []
def naive_find(s, t):
return [i for i in range(len(s) - len(t) + 1) if s[i : i + len(t)] == t]
for _ in range(300):
text = "".join(random.choice("ab") for _ in range(random.randint(0, 30)))
pat = "".join(random.choice("ab") for _ in range(random.randint(1, 5)))
assert kmp_search(text, pat) == naive_find(text, pat)Only extra memory is really needed if you process the text online instead of building the concatenation, which is helpful for streams.
Applications
Smallest period of a string
The string has a period if for all valid . The smallest period is . It divides the length exactly when the string is a repetition of a shorter block:
def shortest_repeating_block(s):
n = len(s)
if n == 0:
return ""
p = n - prefix_function(s)[-1]
return s[:p] if n % p == 0 else s
assert shortest_repeating_block("abcabcabc") == "abc"
assert shortest_repeating_block("abcabca") == "abcabca" # period 3 but does not divide 7
assert shortest_repeating_block("aaaa") == "a"
assert shortest_repeating_block("abab") == "ab"Number of occurrences of each prefix
Every prefix of length ending at is also a suffix, which tells us how often each prefix occurs as a substring. Count the values of , then push the counts down the chain of borders (a border of length implies a border of length ):
def prefix_occurrences(s):
"""occ[k] = number of occurrences of the prefix of length k in s (k = 1..n)."""
n = len(s)
pi = prefix_function(s)
occ = [0] * (n + 1)
for v in pi:
occ[v] += 1
for k in range(n - 1, 0, -1):
occ[pi[k - 1]] += occ[k]
return [c + 1 for c in occ[1:]] # +1: the prefix itself
assert prefix_occurrences("abab") == [2, 2, 1, 1] # a x2, ab x2, aba x1, abab x1
for _ in range(100):
t = "".join(random.choice("ab") for _ in range(random.randint(1, 15)))
expected = [sum(t[i : i + k] == t[:k] for i in range(len(t) - k + 1)) for k in range(1, len(t) + 1)]
assert prefix_occurrences(t) == expectedAll borders of a string
Following enumerates every border, longest first:
def all_borders(s):
pi = prefix_function(s)
out, k = [], pi[-1] if pi else 0
while k > 0:
out.append(k)
k = pi[k - 1]
return out
assert all_borders("abacaba") == [3, 1] # "aba" and "a"
assert all_borders("abc") == []Related
The Z-function is a close cousin with the same applications, and either can be converted into the other in .
Practice problems
- UVA # 455 "Periodic Strings"
- UVA # 11022 "String Factoring"
- UVA # 11452 "Dancing the Cheeky-Cheeky"
- UVA 12604 - Caesar Cipher
- UVA 12467 - Secret Word
- UVA 11019 - Matrix Matcher
- SPOJ - Pattern Find
- SPOJ - A Needle in the Haystack
- Codeforces - Anthem of Berland
- Codeforces - MUH and Cube Walls
- Codeforces - Prefixes and Suffixes