The Leetcode problem Number of Good Ways to Split a String asks you to determine how many ways you can split a given string s
(consisting of only lowercase English letters) into two non-empty parts, such that the number of distinct letters in the left part is equal to the number of distinct letters in the right part.
More formally, you need to count the number of indices i
(where 1 ≤ i < s.length
) such that the substring s[0:i]
and s[i:]
each contain the same number of unique characters.
At first glance, the problem suggests checking every possible split of the string and comparing the number of unique characters on both sides. This brute-force approach would involve, for each split point, creating two substrings and counting their unique letters using, for example, a set. However, this would be too slow for large strings since both substring creation and set operations are costly.
To optimize, we need to avoid recalculating the number of unique characters for each split. Instead, we can precompute or incrementally track the number of unique characters as we move the split point from left to right. This way, we can solve the problem efficiently in a single or double pass.
We can solve the problem efficiently using two passes and some auxiliary data structures:
right_unique
where right_unique[i]
stores the number of unique characters in the substring s[i:]
.s[0:i]
).i
, we compare the count of unique characters on the left (from our tracking structure) with right_unique[i]
.By maintaining counts incrementally, we avoid redundant work and solve the problem in linear time.
Let's walk through the process with the example s = "aacaba"
:
a
(unique: 1), b
(unique: 2), a
(unique: 2), c
(unique: 3), a
(unique: 3), a
(unique: 3)
right_unique = [3, 3, 3, 2, 1, 0]
(last 0 is unused).i
, compare the size of the set (left unique count) with right_unique[i]
:right_unique
array and O(1) for sets (since alphabet size is fixed).The key to solving the "Number of Good Ways to Split a String" problem efficiently is to avoid redundant work by tracking unique character counts incrementally as we move through the string. By making two passes (one from right to left and one from left to right) and using simple data structures to record unique counts, we can solve the problem in linear time. This approach is both elegant and highly efficient, especially for large input sizes.
class Solution:
def numSplits(self, s: str) -> int:
n = len(s)
right_unique = [0] * n
seen = set()
for i in range(n-1, -1, -1):
seen.add(s[i])
right_unique[i] = len(seen)
seen.clear()
ans = 0
left_unique = 0
for i in range(n-1):
seen.add(s[i])
left_unique = len(seen)
if left_unique == right_unique[i+1]:
ans += 1
return ans
class Solution {
public:
int numSplits(string s) {
int n = s.size();
vector<int> right_unique(n, 0);
unordered_set<char> seen;
for (int i = n - 1; i >= 0; --i) {
seen.insert(s[i]);
right_unique[i] = seen.size();
}
seen.clear();
int ans = 0;
for (int i = 0; i < n - 1; ++i) {
seen.insert(s[i]);
int left_unique = seen.size();
if (left_unique == right_unique[i + 1]) {
++ans;
}
}
return ans;
}
};
import java.util.*;
class Solution {
public int numSplits(String s) {
int n = s.length();
int[] rightUnique = new int[n];
Set<Character> seen = new HashSet<>();
for (int i = n - 1; i >= 0; --i) {
seen.add(s.charAt(i));
rightUnique[i] = seen.size();
}
seen.clear();
int ans = 0;
for (int i = 0; i < n - 1; ++i) {
seen.add(s.charAt(i));
int leftUnique = seen.size();
if (leftUnique == rightUnique[i + 1]) {
ans++;
}
}
return ans;
}
}
var numSplits = function(s) {
const n = s.length;
const rightUnique = new Array(n).fill(0);
const seen = new Set();
for (let i = n - 1; i >= 0; --i) {
seen.add(s[i]);
rightUnique[i] = seen.size;
}
seen.clear();
let ans = 0;
for (let i = 0; i < n - 1; ++i) {
seen.add(s[i]);
let leftUnique = seen.size;
if (leftUnique === rightUnique[i + 1]) {
ans++;
}
}
return ans;
};