Number of Good Ways to Split a String
MedPrompt
You are given a string s.
A split is called good if you can split s into two non-empty strings sleft and sright where their concatenation is equal to s (i.e., sleft + sright = s) and the number of distinct letters in sleft and sright is the same.
Return the number of good splits you can make in s.
Example 1:
"aacaba"Example 2:
Input: s = "abcd"
Output: 1
Explanation: Split the string as follows ("ab", "cd").
Constraints:
1 <= s.length <= 105sconsists of only lowercase English letters.
Approaches
3 approaches with complexity analysis and trade-offs.
This approach directly translates the problem statement into code. It iterates through every possible way to split the string into two non-empty parts. For each split, it calculates the number of unique characters in the left and right substrings independently and compares them. While straightforward, this method is inefficient because it repeatedly recalculates character counts for overlapping substrings.
Algorithm
- Initialize a counter
goodSplitsto 0. - Iterate through all possible split points
ifrom1ton-1, wherenis the length of the strings. - For each
i, create the left substrings_left = s.substring(0, i)and the right substrings_right = s.substring(i). - Use a
HashSetto count the number of distinct characters ins_left. - Use another
HashSetto count the number of distinct characters ins_right. - If the two counts are equal, increment
goodSplits. - After the loop finishes, return
goodSplits.
Walkthrough
The algorithm considers every possible split point. A string of length n can be split in n-1 ways. We can loop from i = 1 to n-1, where i is the index where the split occurs. The left part of the string will be s[0...i-1] and the right part will be s[i...n-1].
For each split, we perform the following steps:
- Extract the left substring,
s_left. - Extract the right substring,
s_right. - Create a
HashSetfors_left. Iterate through its characters and add them to the set. The size of the set gives the number of distinct characters. - Do the same for
s_rightwith a newHashSet. - If the sizes of the two sets are equal, we have found a 'good' split, so we increment a counter.
This process is repeated for all n-1 possible splits.
import java.util.HashSet;import java.util.Set; class Solution { public int numSplits(String s) { int n = s.length(); int goodSplits = 0; // Iterate through all possible split points for (int i = 1; i < n; i++) { String s_left = s.substring(0, i); String s_right = s.substring(i); Set<Character> leftChars = new HashSet<>(); for (char c : s_left.toCharArray()) { leftChars.add(c); } Set<Character> rightChars = new HashSet<>(); for (char c : s_right.toCharArray()) { rightChars.add(c); } if (leftChars.size() == rightChars.size()) { goodSplits++; } } return goodSplits; }}Complexity
Time
O(N^2), where N is the length of the string. The outer loop runs N-1 times. Inside the loop, creating substrings and iterating over them to count distinct characters takes O(N) time. Thus, the total time complexity is (N-1) * O(N) = O(N^2).
Space
O(N), where N is the length of the string. In each iteration, new substrings are created, which can take up to O(N) space. The HashSets take O(K) space where K is the alphabet size (a constant, 26), but the substring space dominates.
Trade-offs
Pros
Very simple to understand and implement.
It is a direct and literal interpretation of the problem description.
Cons
Highly inefficient due to repeated work.
Creating substrings and populating hash sets in each iteration is computationally expensive.
This solution will likely result in a 'Time Limit Exceeded' error for inputs that are close to the constraints.
To optimize the brute-force approach, we can avoid re-calculating the distinct character counts in every step. This approach uses precomputation. We make two passes over the string to build two arrays: one storing the count of distinct characters for every possible prefix, and another for every possible suffix. With these arrays, we can find the distinct counts for any left/right split in O(1) time.
Algorithm
- Create an integer array
prefixof sizen. - Create a
HashSetto track unique characters. - Iterate from left to right through
s. For each indexi, adds.charAt(i)to the set and storeset.size()inprefix[i]. - Create an integer array
suffixof sizen. - Clear the
HashSet. - Iterate from right to left through
s. For each indexi, adds.charAt(i)to the set and storeset.size()insuffix[i]. - Initialize
goodSplits = 0. - Iterate from
i = 0ton-2. A split occurs after indexi. - If
prefix[i](distinct chars in left part) equalssuffix[i+1](distinct chars in right part), incrementgoodSplits. - Return
goodSplits.
Walkthrough
The main inefficiency of the brute-force method is the repeated counting of distinct characters. We can eliminate this by pre-calculating these counts.
-
Prefix Counts: We create an array, say
prefix_counts, of the same length as the strings.prefix_counts[i]will store the number of unique characters in the substrings[0...i]. We can compute this array in a single pass from left to right, using aHashSetto keep track of the unique characters seen so far. -
Suffix Counts: Similarly, we create another array,
suffix_counts.suffix_counts[i]will store the number of unique characters in the substrings[i...n-1]. This is computed in a single pass from right to left. -
Counting Good Splits: After both arrays are populated, we can find the number of good splits in a final, single pass. We iterate from
i = 0ton-2. Eachirepresents a potential split point after thei-th character. The number of distinct characters in the left part (s[0...i]) isprefix_counts[i], and for the right part (s[i+1...n-1]) it issuffix_counts[i+1]. If these two values are equal, we increment ourgoodSplitscounter.
import java.util.HashSet;import java.util.Set; class Solution { public int numSplits(String s) { int n = s.length(); int[] prefixDistinct = new int[n]; int[] suffixDistinct = new int[n]; Set<Character> distinctChars = new HashSet<>(); // Calculate prefix distinct counts for (int i = 0; i < n; i++) { distinctChars.add(s.charAt(i)); prefixDistinct[i] = distinctChars.size(); } // Clear set and calculate suffix distinct counts distinctChars.clear(); for (int i = n - 1; i >= 0; i--) { distinctChars.add(s.charAt(i)); suffixDistinct[i] = distinctChars.size(); } int goodSplits = 0; // A split is after index i, so left is s[0...i] and right is s[i+1...n-1] for (int i = 0; i < n - 1; i++) { if (prefixDistinct[i] == suffixDistinct[i + 1]) { goodSplits++; } } return goodSplits; }}Complexity
Time
O(N), where N is the length of the string. We perform three separate passes over the string/arrays (one for prefixes, one for suffixes, one for checking splits), each taking O(N) time. The total time is O(N) + O(N) + O(N) = O(N).
Space
O(N), where N is the length of the string. We use two arrays, `prefixDistinct` and `suffixDistinct`, each of size N.
Trade-offs
Pros
Much more efficient than brute force, with a linear time complexity.
The logic is still relatively easy to follow: precompute, then check.
Cons
Requires extra space proportional to the input size, which might be a concern for very large inputs under strict memory constraints.
This is the most optimal approach, achieving linear time complexity with constant extra space. Instead of precomputing and storing all prefix/suffix counts, we can iterate through the string once. We maintain the character counts for the left and right partitions and dynamically update them as we slide the split point from left to right.
Algorithm
- Create two frequency arrays,
leftFreqandrightFreq, of size 26, initialized to 0. - Initialize
leftDistinct = 0andrightDistinct = 0. - First, iterate through the entire string
sto populaterightFreqand calculate the initialrightDistinctcount. - Initialize
goodSplits = 0. - Iterate through the string from
i = 0ton-2to simulate moving the split point. Let the current character bec. - For each character
c, move it from the right partition to the left:- Increment
leftFreq[c - 'a']. If its count becomes 1, incrementleftDistinct. - Decrement
rightFreq[c - 'a']. If its count becomes 0, decrementrightDistinct.
- Increment
- After updating the counts, if
leftDistinctequalsrightDistinct, incrementgoodSplits. - Return
goodSplits.
Walkthrough
This method improves upon the previous one by reducing the space complexity from O(N) to O(1).
-
Initialization: We start by assuming the split is before the first character. This means the left part is empty, and the right part is the entire string. We use two frequency arrays of size 26 (for lowercase English letters),
leftFreqandrightFreq. We first populaterightFreqby counting all characters ins. We also count the number of distinct characters ins, let's call thisrightDistinct. -
Single Pass Iteration: We then iterate from left to right through the string, from index
i = 0ton-2. In each step, we are essentially moving the characters.charAt(i)from the right partition to the left partition.- Let
c = s.charAt(i). - Update Left Partition: We update
leftFreqfor characterc. Ifcis being added to the left partition for the first time (i.e., its count inleftFreqwas 0), we increment aleftDistinctcounter. - Update Right Partition: We update
rightFreqfor characterc. If the count ofcinrightFreqbecomes 0 after decrementing, it means this character is now completely gone from the right partition, so we decrement therightDistinctcounter. - Check for Good Split: After each move, we compare
leftDistinctandrightDistinct. If they are equal, it means the current split (after characterc) is a good one, and we increment our result counter.
- Let
This way, we find all good splits in a single pass with only constant extra space for the frequency arrays.
class Solution { public int numSplits(String s) { int n = s.length(); int[] leftFreq = new int[26]; int[] rightFreq = new int[26]; int leftDistinct = 0; int rightDistinct = 0; // Initialize rightFreq and rightDistinct for the whole string for (char c : s.toCharArray()) { if (rightFreq[c - 'a'] == 0) { rightDistinct++; } rightFreq[c - 'a']++; } int goodSplits = 0; // Iterate through possible split points, moving one char from right to left for (int i = 0; i < n - 1; i++) { char c = s.charAt(i); // Update left side if (leftFreq[c - 'a'] == 0) { leftDistinct++; } leftFreq[c - 'a']++; // Update right side rightFreq[c - 'a']--; if (rightFreq[c - 'a'] == 0) { rightDistinct--; } // Check for good split if (leftDistinct == rightDistinct) { goodSplits++; } } return goodSplits; }}Complexity
Time
O(N), where N is the length of the string. We have an initial pass to populate the right frequency map (O(N)) and then a single main loop that iterates N-1 times (O(N)). The total time complexity is linear.
Space
O(1). The space required is for two frequency arrays of size 26 and a few integer variables. This is constant and does not scale with the length of the input string `s`.
Trade-offs
Pros
Optimal time complexity of O(N).
Optimal space complexity of O(1), as the space used does not depend on the input size.
Most efficient solution for the given constraints.
Cons
The logic is slightly more complex than the precomputation approach, as it involves managing the state of two partitions simultaneously.
Solutions
Solution
class Solution { public int numSplits ( String s ) { Map < Character , Integer > cnt = new HashMap <>(); for ( char c : s . toCharArray ()) { cnt . merge ( c , 1 , Integer: : sum ); } Set < Character > vis = new HashSet <>(); int ans = 0 ; for ( char c : s . toCharArray ()) { vis . add ( c ); if ( cnt . merge ( c , - 1 , Integer: : sum ) == 0 ) { cnt . remove ( c ); } if ( vis . size () == cnt . size ()) { ++ ans ; } } return ans ; } }class Solution {public: int numSplits(string s) { unordered_map<char, int> cnt; for (char &c : s) { ++cnt[c]; } unordered_set<char> vis; int ans = 0; for (char &c : s) { vis.insert(c); if (--cnt[c] == 0) { cnt.erase(c); } ans += vis.size() == cnt.size(); } return ans; }};class Solution: def numSplits(self, s: str) -> int: cnt = Counter(s) vis = set() ans = 0 for c in s: vis . add(c) cnt[c] -= 1 if cnt[c] == 0: cnt . pop(c) ans += len(vis) == len(cnt) return ansVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.