38 · Segment Trees, Fenwick & String Matching
#1044 Longest Duplicate Substring
frequently reported atByteDance / TikTokGoogleAmazonGoldman SachsMeta
the problem
In plain words
Return any duplicated substring (one that occurs at least twice, occurrences may overlap) with the maximum possible length. Return "" if there is none.
in s = "banana"
out "ana"
in s = "abcd"
out ""
- 2 ≤ s.length ≤ 3·10⁴
- Lowercase English letters
your turn
Try it before you peek
Read the examples, say the brute force out loud, then write code: here or on LeetCode ↗. Stuck for 20 minutes? Open one hint at a time.
Solve it in the browser
LeetCode-style: complete the class. Compiled by the real javac (Java 17) and judged right here — nothing is sent anywhere.
Solutions are hidden so you can try first. Even 20 minutes of real effort makes the answer stick far better than reading it.
Marking the problem solved, or opening every hint, also shows it.
the aha
Key insight
patternBinary search on the answerRolling hash (Rabin–Karp)
If a duplicate of length L exists, one of length L−1 does too (drop a character). That monotonicity lets you binary search the length; a rolling hash checks one length in O(n).
solutions
From brute force to optimal
From the longest length down, put every substring in a HashSet; the first repeat wins.
- For len = n−1 down to 1.
- Add each substring of that length to a set; a failed add is a duplicate.
n lengths × n substrings × O(len) to build and hash each.
One length's substrings stored at a time: up to n strings of up to n chars.
class Solution { public String longestDupSubstring(String s) { int n = s.length(); for (int len = n - 1; len > 0; len--) { // longest length first Set<String> seen = new HashSet<>(); for (int i = 0; i + len <= n; i++) { String sub = s.substring(i, i + len); if (!seen.add(sub)) return sub; // add() is false when already present } } return ""; }}Binary search the length; check each length with a set of real substrings.
- lo = 1, hi = n − 1.
- If a duplicate of length mid exists → record it, go longer; else go shorter.
log n lengths × n substrings × O(n) each.
The substrings in the set.
class Solution { public String longestDupSubstring(String s) { String best = ""; int lo = 1, hi = s.length() - 1; while (lo <= hi) { // a duplicate of length L implies one of length L-1 int mid = lo + (hi - lo) / 2; String dup = findDuplicate(s, mid); if (dup != null) { best = dup; lo = mid + 1; } else hi = mid - 1; } return best; } private String findDuplicate(String s, int len) { Set<String> seen = new HashSet<>(); for (int i = 0; i + len <= s.length(); i++) { String sub = s.substring(i, i + len); // O(len) to build and to hash if (!seen.add(sub)) return sub; } return null; }}Same binary search, but store rolling hashes (with start indices to verify collisions).
- For a length len: hash the first window, then roll across s.
- Map hash → list of starts; on a hit, confirm with regionMatches.
- Binary search on len as before.
log n lengths × O(n) rolling per length; verification is rare except for true matches, which end the scan.
At most n hashes and starts per length.
class Solution { private static final long MOD = 1_000_000_007L, BASE = 131; public String longestDupSubstring(String s) { int lo = 1, hi = s.length() - 1, bestStart = -1, bestLen = 0; while (lo <= hi) { int mid = lo + (hi - lo) / 2; int start = findDuplicate(s, mid); if (start >= 0) { bestStart = start; bestLen = mid; lo = mid + 1; } else hi = mid - 1; } return bestStart < 0 ? "" : s.substring(bestStart, bestStart + bestLen); } /** Start of a substring of length len that appeared earlier, or -1. */ private int findDuplicate(String s, int len) { long high = 1, hash = 0; for (int i = 1; i < len; i++) high = high * BASE % MOD; // BASE^(len-1) for (int i = 0; i < len; i++) hash = (hash * BASE + s.charAt(i)) % MOD; Map<Long, List<Integer>> seen = new HashMap<>(); seen.computeIfAbsent(hash, k -> new ArrayList<>()).add(0); for (int i = 1; i + len <= s.length(); i++) { hash = (hash - s.charAt(i - 1) * high % MOD + MOD) % MOD; // drop the left char hash = (hash * BASE + s.charAt(i + len - 1)) % MOD; // add the right char List<Integer> starts = seen.computeIfAbsent(hash, k -> new ArrayList<>()); for (int j : starts) if (s.regionMatches(j, s, i, len)) return i; // same hash AND same text starts.add(i); } return -1; }}in the room
How to say it in the interview
talk track
- Spot the monotonic property first, then binary search the length — that's half the solution.
- Then: "checking one length with real strings is O(n²); a rolling hash makes it O(n)."
- Mention collisions and how you verify.
traps
- Skipping verification and returning a substring that only hashes the same.
- int overflow in hash arithmetic — use long.
what they ask next
Follow-ups
Deterministic O(n log n) or O(n)?
A suffix array with the LCP array: the answer is the maximum LCP of adjacent suffixes.
before you submit
Edge cases to test
- No repeated character → ""
- Overlapping copies count: "aaaaa" → "aaaa"
- Hash collisions: two different substrings with the same hash must not be reported
- Two equal letters, "aa" → "a"
practise next
Similar problems
- #1392Longest Happy PrefixHardin Range Queries & Strings
- #875Koko Eating BananasMediumin Binary Search
- #28Find the Index of the First Occurrence in a StringEasyin Range Queries & Strings