Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
Count how many distinct (unique) non-empty substrings a string has. There are n(n+1)/2 substrings counting duplicates; the answer subtracts the repeats. The clean optimal: build the suffix array, then the LCP array (Kasai), and compute n(n+1)/2 − Σ LCP[i]. Each suffix contributes (its length) new substrings minus the prefix it shares with the previous suffix in sorted order. That's O(n log n) for the suffix array plus O(n) for LCP.
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you can move past the brute-force hash set to the suffix-array + LCP formula, and whether you can explain why adjacent sorted suffixes capture all duplicate substrings.
Where candidates slip
Stopping at O(n²) hashing (and forgetting it's also O(n²) memory / collision-prone), or miscounting by forgetting that LCP is taken between adjacent suffixes in sorted order only.
How to stand out
Derive distinct = n(n+1)/2 − ΣLCP from first principles, mention the suffix automaton as the O(n) alternative, and note the same machinery solves longest-repeated-substring.
Choose
Compare the available approaches
Use constraints and trade-offs to select the route you can defend in an interview.
Interactive decision treeInteractiveChoose the right approachExplore the full treeCollapse the tree
How to read it:Substrings are prefixes of suffixes; sorting suffixes makes duplicate prefixes adjacent so LCP counts the repeats.
Learn & trace
Build each solution step by step
Move from the core idea to a visual trace, implementation, proof, and edge cases.
Approach 1 of 2
Brute Force — Hash Every Substring
1Hint 1Explore hintHide hint
Enumerate every substring s[i..j] and insert it into a hash set; the set's size is the answer.
2Hint 2Explore hintHide hint
There are about n²/2 substrings. Inserting each O(1)-ish with a precomputed rolling hash gives O(n²); inserting raw strings costs an extra O(n) each.
3Hint 3Explore hintHide hint
Both the time and the memory grow as n² — that's the ceiling that suffix structures break.
A · Core idea
The definition-driven solution: produce every substring and let a set deduplicate.
The literal method. Loop i over start indices and j over end indices, and add s[i..j] to a set. After the loops, the set's cardinality is the count of distinct substrings.
Making it O(n²) instead of O(n³). Inserting the substring string costs O(length) for hashing and comparison, so the literal version is O(n³). Precompute prefix hashes so any substring's polynomial hash is O(1); store hashes in the set. That removes the per-substring length factor and gives O(n²) time — but introduces collision risk, so a fully correct version would verify or use double hashing.
Why it doesn't scale. Both time and memory are Θ(n²). At n = 10⁵ that's 10¹⁰ entries — neither the clock nor the heap survives. The approach is correct and is a fine sanity check for small n, but it enumerates exactly the thing we want to avoid enumerating.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
s = "abab"Hash-map state
after len 1
Continue scanning
a and b inserted, duplicates ignored
7C · Code with teaching comments
Reference implementation previewimport java.util.HashSet; import java.util.Set; public class BruteForceDistinct { public long countDistinct(String s) {Explore the full codeHide the full code
D · Why it works and complexityO(n²) average (hashing), O(n³) if comparing strings timeO(n²) spaceOpen the invariant and proofExplore the proofHide the proof
Hashing every substring treats each substring as independent, ignoring that substrings overlap massively. The suffix-array view exploits that overlap: equal substrings become equal prefixes of suffixes, which cluster when sorted.
ComplexityO(n²) average (hashing), O(n³) if comparing strings time, O(n²) space — n(n+1)/2 substrings. With a rolling/prefix hash, each substring's hash is O(1) to derive and O(1) to insert → O(n²) time. Storing them is O(n²) space. Comparing actual strings instead of hashes would add an O(n) factor (O(n³)).
E · Edge cases and pitfalls3Returning an int — the count overflows 32-bit for large n.Explore edge casesHide edge cases
Pitfalls
- Returning an int — the count overflows 32-bit for large n.
- Believing the hash-set version is O(n) memory — it's O(n²).
- Using raw string hashing without verification and assuming zero collisions.
Approach 2 of 2 · Optimal
Suffix Array + LCP (Optimal)
1Hint 1Explore hintHide hint
Reframe: every substring is a prefix of some suffix of s. So counting distinct substrings = counting distinct prefixes across all suffixes.
2Hint 2Explore hintHide hint
If you sort the suffixes, two suffixes that share a long prefix sit next to each other. The shared prefix is exactly the duplicated substrings between them.
3Hint 3Explore hintHide hint
Suffix i alone contributes (n − SA[i]) prefixes; subtract LCP[i], the prefix it shares with the previous sorted suffix, to avoid double counting. Summing gives n(n+1)/2 − ΣLCP.
A · Core idea
The optimal solution rests on two reframings and one formula.
Reframing 1 — substrings are prefixes of suffixes. Any substring s[i..j] is the prefix of length j−i+1 of the suffix that starts at i. So the set of all substrings equals the set of all prefixes of all suffixes. Counting distinct substrings becomes counting distinct prefixes over every suffix.
Reframing 2 — sort the suffixes. Build the suffix array SA: the indices of all n suffixes sorted lexicographically. Why sort? Because two suffixes that begin with the same characters end up adjacent, so any prefix shared by two different suffixes is shared by adjacent sorted suffixes. That localizes all duplication to neighboring pairs.
The LCP array does the counting. LCP[i] is the length of the longest common prefix of SA[i] and SA[i−1] (consecutive sorted suffixes). Consider the suffixes in sorted order. The suffix at SA[i] has length n − SA[i], contributing that many prefixes. But the first LCP[i] of those prefixes already appeared as prefixes of the previous suffix SA[i−1] — they're duplicates. So this suffix contributes (n − SA[i]) − LCP[i] new distinct substrings.
The formula. Summing the new contributions over all suffixes, the lengths sum to n(n+1)/2 (each suffix's full length, which is just the gross count of all substrings) and the LCPs sum to the total duplication: distinct = n(n+1)/2 − Σ LCP[i]. The first suffix has no predecessor, so its LCP is 0 and it contributes its full length.
Building the pieces efficiently. The suffix array uses prefix doubling: rank suffixes by their first character, then repeatedly double the compared length using previous ranks, re-sorting by (rank[i], rank[i+k]) pairs. That's O(n log n) ranks with O(log n) rounds. Kasai's algorithm computes LCP in O(n) by processing suffixes in original-string order and reusing the fact that the LCP can shrink by at most one when you move from suffix i to suffix i+1.
An aside on the O(n) variant. A suffix automaton counts distinct substrings in true O(n) by summing len(state) − len(link(state)) over states. Mention it as the asymptotically tighter option; the suffix-array route is the more commonly expected derivation in interviews.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
s = "abab"Array state
SA order
"ab", "abab", "b", "bab"
7C · Code with teaching comments
D · Why it works and complexityO(n log n) timeO(n) spaceOpen the invariant and proofExplore the proofHide the proof
The leap is seeing that 'distinct substrings' equals 'distinct prefixes of suffixes', because then sorting makes all duplicate prefixes adjacent and the LCP array measures exactly the over-count. The whole problem collapses to one subtraction: gross count minus shared prefixes.
ComplexityO(n log n) time, O(n) space — Suffix array by prefix-doubling is O(n log n) (O(n log² n) with comparison sort, O(n log n) with radix). Kasai's LCP is O(n) amortized — the helper length h drops by at most 1 between steps. The final sum is O(n). Overall O(n log n) time, O(n) space.
E · Edge cases and pitfalls7s = "aaa" — SA = [2,1,0]; LCP = [0,1,2]; 6 − 3 = 3 distinct.Explore edge casesHide edge cases
Edge cases handled
s = "aaa"SA = [2,1,0]; LCP = [0,1,2]; 6 − 3 = 3 distinct.s = "a"n=1, gross = 1, ΣLCP = 0 → 1.all distinct chars "abc"ΣLCP = 0 → n(n+1)/2 = 6, every substring unique.
Pitfalls
- Taking LCP between non-adjacent suffixes — only adjacent sorted pairs are correct.
- Using a 32-bit accumulator for n(n+1)/2 — overflows for large n.
- Computing rank from SA incorrectly in Kasai — rank[sa[i]] = i, not the reverse.
- Forgetting that the first sorted suffix contributes its full length (LCP[0] = 0).
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewimport java.util.Arrays; public class DistinctSubstrings { public int[] suffixArray(String s) { int n = s.length();Explore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1Stopping at the hash-set baseline
It enumerates all n²/2 substrings in both time and memory. For n = 10⁵ that is 10¹⁰ entries — it will not finish or fit. The interviewer expects the suffix-array reduction.
Explore the code comparisonHide the code comparison
Stopping at the hash-set baseline
It enumerates all n²/2 substrings in both time and memory. For n = 10⁵ that is 10¹⁰ entries — it will not finish or fit. The interviewer expects the suffix-array reduction.
Don't write this
seen = {s[i:j] for i in range(n) for j in range(i + 1, n + 1)}
return len(seen) # O(n^2) time AND memoryWrite this instead
sa = suffix_array(s)
lcp = kasai_lcp(s, sa)
return n * (n + 1) // 2 - sum(lcp)2Wrong rank inversion in Kasai
Kasai needs rank[i] = position of suffix i in SA. Writing sa[rank[i]] or inverting the assignment gives garbage LCPs.
Explore the code comparisonHide the code comparison
Wrong rank inversion in Kasai
Kasai needs rank[i] = position of suffix i in SA. Writing sa[rank[i]] or inverting the assignment gives garbage LCPs.
Don't write this
for (int i = 0; i < n; i++) rank[i] = sa[i]; // WRONG directionWrite this instead
for (int i = 0; i < n; i++) rank[sa[i]] = i; // rank is the inverse of SA332-bit overflow on the gross count
n(n+1)/2 reaches ~5×10⁹ at n = 10⁵, past the 32-bit limit, so the subtraction underflows into a wrong (often negative) answer.
Explore the code comparisonHide the code comparison
32-bit overflow on the gross count
n(n+1)/2 reaches ~5×10⁹ at n = 10⁵, past the 32-bit limit, so the subtraction underflows into a wrong (often negative) answer.
Don't write this
int total = n * (n + 1) / 2; // overflowsWrite this instead
long total = (long) n * (n + 1) / 2;Revise
Carry the pattern into the interview
Pattern to remember
Suffix array + LCP: total substrings minus shared prefixes
Code pattern to memorize
distinct = n*(n+1)/2 - sum(LCP[i] for i in 1..n-1)
where SA = sorted suffixes, LCP[i] = longest common prefix of SA[i] and SA[i-1]Rules to remember
- 1Every substring is a prefix of some suffix — that reframing is the whole reason suffixes show up.
- 2Sort the suffixes; adjacent sorted suffixes share the most prefix, so
LCP[i]counts exactly the duplicate substrings to subtract. - 3Total substrings with duplicates is
n(n+1)/2; distinct = that minusΣ LCP. - 4The O(n²) hashing baseline stores every substring's hash in a set — correct, but O(n²) time and space, and risks collisions without verification.
- 5Use a separator-free input here; suffix arrays don't need one, unlike the Z/KMP concatenation trick.
Use this pattern when
- Counting unique substrings, or unique substrings of a given length
- Any problem reducible to 'distinct prefixes of suffixes'
- Longest repeated substring, longest common substring of two strings
- When you already need a suffix array/automaton for other queries
Choose another pattern when
- n is tiny (≤ a few thousand) and you just need a quick answer → the hashing set is simpler to write
- You need online/streaming updates → suffix automaton incremental construction
- Only counting occurrences of one fixed pattern → KMP/Z, not a full suffix structure
- Memory is extremely tight → suffix array uses O(n) ints; a hash set of substrings is far worse
Interview takeaway
Two reframings carry the whole problem: substrings are prefixes of suffixes, and sorting suffixes makes duplicate prefixes adjacent. The LCP array then turns 'count distinct' into one subtraction.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternSuffix Array + LCP. Recognise it for any 'distinct / repeated / common substring' question: the SA clusters equal prefixes and the LCP array measures their overlap, turning enumeration into arithmetic.
Practice queueDirect variations and pattern siblings
Direct follow-ups
- Longest Repeated SubstringComing soonThe maximum value in the LCP array is the length of the longest substring that appears at least twice.
- Longest Common Substring of Two StringsComing soonConcatenate with a separator, build SA+LCP, take the max LCP between suffixes originating from different strings.
- Count Distinct Substrings of Each LengthComing soonUse the LCP array bucketed by length, or a suffix automaton's per-state contributions.
- Distinct Substrings via Suffix Automaton (O(n))Coming soonSum len(state) − len(link(state)) over all states for a true linear-time count.