Longest Duplicate Substring is a beautiful "two algorithms stacked on top of each other" problem. It binary searches over a length, and uses a rolling hash (Rabin-Karp) to answer each yes/no question fast. Master it and you understand both monotonic binary search and string hashing at once.
Problem. Given a string s, return any longest substring that appears at least twice in s
(the two occurrences may overlap). If no substring repeats, return the empty string.
Example: s = "banana" → answer "ana" (it appears at index 1 and again at index 3).
The slow way first
The obvious idea: check every possible substring and see if it shows up elsewhere. There are about O(n²) substrings and comparing each one costs O(n), so this is roughly O(n³) — far too slow for a long string.
The question to ask: what makes this hard? Two things at once — we do not know the answer length, and for a given length, checking for a repeat naively is expensive. We attack each separately.
The idea: binary search the length, hash to check it
Here is the key insight: if a duplicate of length L exists, then a duplicate of every shorter length exists too (just take a prefix of it). That makes "is there a duplicate of length L?" a monotonic yes/no — true for small L, false for large L — so we can binary search the largest L that still answers yes.
To answer one question — is there any repeated substring of exactly length L? — slide a window of length L across s, compute each window's rolling hash, and drop the hashes into a set. A repeat hash means a candidate duplicate.
A rolling hash lets each window's hash be computed from the previous one in O(1): drop the leading character and append the trailing one. So one length check is O(n) total, not O(n·L).
Walk through it
Step through the animation on "banana". The lo and hi pointers bound the candidate length. We try L = 3 first, find that "ana" repeats, so we search longer (lo = mid + 1). Length 4 has no repeat, so we shrink from the right (hi = mid − 1). The window collapses and we return the best find, "ana".
Pseudocode
lo, hi = 1, len(s) - 1 # candidate lengths
best = ""
while lo <= hi:
mid = (lo + hi) // 2
dup = find_dup_of_length(s, mid) # rolling hash over all length-mid windows
if dup is not None:
best = dup # length mid works, try longer
lo = mid + 1
else:
hi = mid - 1 # too long, try shorter
return bestThe Python solution
def longest_dup_substring(s):
lo, hi = 1, len(s) - 1
best = ""
while lo <= hi:
mid = (lo + hi) // 2
dup = find_dup_of_length(s, mid)
if dup is not None:
best = dup
lo = mid + 1
else:
hi = mid - 1
return bestlo, hibound the length we are searching for, from 1 up tolen(s) - 1.midis the length we test this iteration.find_dup_of_length(s, mid)slides a length-midwindow with a rolling hash and returns a repeated substring, orNone.- If a duplicate exists, we record it and go longer (
lo = mid + 1) — longer answers are still possible. - If not, we go shorter (
hi = mid - 1). - When
lo > hithe search ends andbestholds the longest duplicate found.
Complexity
| Case | Time | Notes |
|---|---|---|
| Brute force (all substrings) | O(n^3) (moderate) | O(n^2) substrings times O(n) compare |
| Binary search + rolling hash | O(n log n) (moderate) | log n length checks, each O(n) |
O(n) (moderate)We do O(log n) binary-search steps, and each step scans the string once with a rolling hash in O(n) — giving O(n log n). The hash set holds up to O(n) window hashes, so space is O(n).
When this pattern shows up
Whenever a problem asks for the longest / largest something and "can we achieve size X?" gets harder as X grows, that monotonicity means you can binary search the answer and only solve the easier decision problem. Pair it with a rolling hash whenever the decision is about repeated substrings.
A hash match is only a candidate — two different substrings can share a hash (a collision). In an interview, mention that you would verify the actual characters match, or use double hashing, to avoid a false positive.
Practice
In banana, after L = 3 succeeds with ana, what does lo become and which lengths do we still test?
1. Why can we binary search over the length L?
2. What does the rolling hash give us at each length check?
3. When find_dup_of_length returns a duplicate, what do we do?
4. What is the overall time complexity?