String Algorithms contents
Finding Repetitions (Main-Lorentz)
Find all squares (a substring written twice in a row) in O(n log n) with divide and conquer and the Z-function.
Read first: Z-function
A repetition (or square) is a substring made of two identical halves, like abab or ee. The task: find all repetitions in a string , or just the longest one, or any one.
For acababaee the repetitions are abab (positions 2-5), baba (3-6) and ee (7-8). For abaaba: abaaba (0-5) and aa (2-3).
A string can contain repetitions (aaaa...a has one for every even-length substring), so the algorithm reports them in a compressed form: groups of repetitions described by a few numbers. Expanding those groups gives the pairs of positions, at the price of the output size.
Main-Lorentz algorithm
Divide and conquer. Split into halves, find the repetitions lying completely in and completely in recursively, and then find the crossing repetitions, those that start in and end in . If the crossing ones are found in , the total is .
Crossing repetitions
Look at the middle of a crossing repetition (the first character of its second half). It is either in (a left repetition) or in (a right one). Consider the left repetitions; right ones are symmetric.
The first character of the repetition that falls into is at position and equals the character exactly one half-length earlier, at position . Fix ; this fixes the half-length . All the repetitions found for this have length , and differ only in where the middle falls relative to .
Split the half into two parts of lengths (up to exclusive) and (from ). A repetition exists for the pair iff
- = the longest common suffix of and satisfies , and
- = the longest common prefix of and satisfies .
So we need , and then every gives one repetition. Both and come from Z-functions: from the Z-function of the reversed , and from the Z-function of . For right repetitions use the Z-functions of and .
Implementation
def z_function(s):
n = len(s)
z = [0] * n
l = r = 0
for i in range(1, n):
if i < r:
z[i] = min(r - i, z[i - l])
while i + z[i] < n and s[z[i]] == s[i + z[i]]:
z[i] += 1
if i + z[i] > r:
l, r = i, i + z[i]
return z
def get_z(z, i):
return z[i] if 0 <= i < len(z) else 0
def find_repetitions(s):
"""All repetitions as (start, end) inclusive index pairs, found with Main-Lorentz."""
repetitions = []
def convert(shift, left, cntr, l, k1, k2):
for l1 in range(max(1, l - k2), min(l, k1) + 1):
if left and l1 == l:
break
pos = shift + (cntr - l1 if left else cntr - l - l1 + 1)
repetitions.append((pos, pos + 2 * l - 1))
def solve(t, shift):
n = len(t)
if n == 1:
return
nu = n // 2
nv = n - nu
u, v = t[:nu], t[nu:]
ru, rv = u[::-1], v[::-1]
solve(u, shift)
solve(v, shift + nu)
z1 = z_function(ru)
z2 = z_function(v + "#" + u)
z3 = z_function(ru + "#" + rv)
z4 = z_function(v)
for cntr in range(n):
if cntr < nu:
l = nu - cntr
k1 = get_z(z1, nu - cntr)
k2 = get_z(z2, nv + 1 + cntr)
else:
l = cntr - nu + 1
k1 = get_z(z3, nu + 1 + nv - 1 - (cntr - nu))
k2 = get_z(z4, cntr - nu + 1)
if k1 + k2 >= l:
convert(shift, cntr < nu, cntr, l, k1, k2)
if len(s) > 1:
solve(s, 0)
return repetitions
assert sorted(find_repetitions("acababaee")) == [(2, 5), (3, 6), (7, 8)]
assert sorted(find_repetitions("abaaba")) == [(0, 5), (2, 3)]
assert find_repetitions("abc") == [] and find_repetitions("a") == []The separator # must not occur in ; for arbitrary alphabets use a value outside it.
Testing against brute force
Enumerate every start and half-length and compare the halves:
import random
def repetitions_brute(s):
n = len(s)
return [(i, i + 2 * l - 1) for i in range(n) for l in range(1, (n - i) // 2 + 1)
if s[i:i + l] == s[i + l:i + 2 * l]]
random.seed(1)
for _ in range(600):
s = "".join(random.choice("ab") for _ in range(random.randint(0, 30)))
found = find_repetitions(s)
assert sorted(found) == sorted(repetitions_brute(s)), s
assert len(found) == len(set(found)) # every repetition is reported onceThe lists agree on all random binary strings (the hardest alphabet, since repetitions are plentiful), and no repetition is reported twice.
Complexity
The recursion has depth , and each level does work computing four Z-functions of total length , so finding all groups takes . If the output must list every repetition explicitly, the running time is , and the output can be quadratic (a string of equal letters).
assert len(find_repetitions("a" * 12)) == sum((12 - 2 * l + 1) for l in range(1, 7)) # 36 squaresVariations
- Longest repetition or any repetition: don't expand the groups; for each with record (the length is the same for the whole group).
- Number of distinct squares in a string is at most (a known bound); counting distinct ones (not occurrences) requires a slightly different approach (runs / Lyndon roots).
- Primitive repetitions (whose half is not itself a repetition) number in total, and Fibonacci strings reach this bound.