Problem
Given a string, count how many ways you can cut it into a non-empty left part and a non-empty right part such that both halves contain the same number of distinct characters.
- Input:
s = "aacaba" - Output:
2 - Explanation: Cutting at index 3 ("aac" | "aba") gives 2 vs 2, and at index 4 ("aaca" | "ba") gives 2 vs 2 β both good.
A counter-example: cutting at index 1 ("a" | "acaba") gives 1 distinct vs 3 distinct β not a good split.
Intuition
The key observation is that as you scan left to right, the distinct character count on the left can only increase (or stay the same), and the count on the right can only decrease. This monotonic structure means you can pre-compute both sides in a single pass each, turning the problem into a simple array comparison.
Approach 1 β Brute Force
For each of the nβ1 valid split positions, independently build a character set for the left and right halves and compare their sizes.
- Iterate split positions from 1 to nβ1.
- Build a set from the left slice s[0..splitβ1] and another from the right slice s[split..nβ1].
- If the two set sizes are equal, count it as a good split.
1def numSplits(s: str) -> int:
2 count = 0
3 for split in range(1, len(s)):
4 left_distinct = len(set(s[:split]))
5 right_distinct = len(set(s[split:]))
6 if left_distinct == right_distinct:
7 count += 1
8 return countTime: O(nΒ²) β rebuilding both character sets from scratch at each of the nβ1 split points.
Space: O(n) β the character sets are bounded by 26 lowercase letters (O(1)), but the string slices themselves take O(n).
Approach 2 β Prefix and Suffix Arrays
Pre-compute two arrays: how many distinct characters exist from the left up to each index, and from the right down to each index. Then a single linear scan over split positions answers the question in O(1) per query.
- Build
prefix_distinct[i]= number of distinct characters in s[0..i] by scanning left to right, maintaining a running set and recording its size at each position. - Build
suffix_distinct[i]= number of distinct characters in s[i..nβ1] by scanning right to left the same way. - For each split position (from 1 to nβ1), check if
prefix_distinct[splitβ1] == suffix_distinct[split]; if so, increment the count.
1def numSplits(s: str) -> int:
2 n = len(s)
3 prefix_distinct = [0] * n
4 suffix_distinct = [0] * n
5
6 seen = set()
7 for i in range(n):
8 seen.add(s[i])
9 prefix_distinct[i] = len(seen)
10
11 seen = set()
12 for i in range(n - 1, -1, -1):
13 seen.add(s[i])
14 suffix_distinct[i] = len(seen)
15
16 count = 0
17 for split in range(1, n): # left = s[0..split-1], right = s[split..n-1]
18 if prefix_distinct[split - 1] == suffix_distinct[split]:
19 count += 1
20 return countTime: O(n) β three linear passes over the string.
Space: O(n) β two arrays of length n; the running sets hold at most 26 entries (effectively O(1)).
Complexity Summary
| Approach | Time | Space | When to use |
|---|---|---|---|
| Brute Force | O(nΒ²) | O(n) | Only for very short strings or manual verification |
| Prefix & Suffix Arrays | O(n) | O(n) | Default β three clean linear passes, no tricks required |
Common Mistakes
- Off-by-one on the split index β the left half is
s[0..splitβ1]and the right half iss[split..nβ1]; usingprefix_distinct[split]instead ofprefix_distinct[splitβ1]includes one extra character on the wrong side. - Allowing empty halves β valid split positions run from 1 to nβ1 (inclusive); splitting at 0 or n leaves one side empty, which the problem forbids.
- Rebuilding the set from scratch per split in the brute force β slicing the string and re-inserting all characters on every iteration is the source of the O(nΒ²) cost; pre-building the arrays is what drops it to O(n).
- Confusing distinct-character count with character frequency β the problem asks how many unique characters appear, not how many times each appears; a
set(or counting nonzero entries in a frequency array) is the right structure. - Forgetting to reset
seenbetween the two passes β reusing the same set for the suffix scan without clearing it will carry over characters from the prefix scan and produce wrong suffix counts.
Related Problems
subarray-sum-equals-kβ same prefix-array technique to answer split-point queries in O(1) after an O(n) precomputationcontiguous-arrayβ uses prefix counts to detect a balanced split property across an indexfind-all-anagrams-in-a-stringβ comparing character-set properties between two substrings using sliding windowspermutation-in-stringβ tracking the distinct character profile of a window and comparing it to a targetlongest-substring-without-repeating-charactersβ expanding and shrinking a window while maintaining a live distinct-character count