Given a string s, return the maximum number of unique substrings that the given string can be split into.
You can split string s into any list of non-empty substrings, where the concatenation of the substrings forms the original string. However, you must split the substrings such that all of them are unique.
A substring is a contiguous sequence of characters within a string.
Example 1:
Input: s = "ababccc" Output: 5 Explanation: One way to split maximally is ['a', 'b', 'ab', 'c', 'cc']. Splitting like ['a', 'b', 'a', 'b', 'c', 'cc'] is not valid as you have 'a' and 'b' multiple times.
Example 2:
Input: s = "aba" Output: 2 Explanation: One way to split maximally is ['a', 'ba'].
Example 3:
Input: s = "aa" Output: 1 Explanation: It is impossible to split the string any further.
Constraints:
-
1 <= s.length <= 16 -
scontains only lower case English letters.
Approach 01:
-
C++
-
Python
#include <string>
#include <unordered_set>
class Solution {
public:
int maxUniqueSplit(std::string s) {
size_t maxUniqueParts = 0;
findMaxUniqueSplit(s, 0, {}, maxUniqueParts);
return maxUniqueParts;
}
private:
void findMaxUniqueSplit(const std::string& str, int startIndex, std::unordered_set<std::string>&& uniqueSet, size_t& maxUniqueParts) {
if (startIndex == str.length()) {
maxUniqueParts = std::max(maxUniqueParts, uniqueSet.size());
return;
}
for (int i = 1; startIndex + i <= str.length(); ++i) {
const std::string currentPart = str.substr(startIndex, i);
if (uniqueSet.find(currentPart) != uniqueSet.end())
continue;
uniqueSet.insert(currentPart);
findMaxUniqueSplit(str, startIndex + i, std::move(uniqueSet), maxUniqueParts);
uniqueSet.erase(currentPart);
}
}
};
class Solution:
def maxUniqueSplit(self, s: str) -> int:
# Use a set to track unique substrings
return self.findMaxUniqueSplit(s, 0, set())
def findMaxUniqueSplit(self, s: str, startIndex: int, uniqueSet: set) -> int:
# If we have processed the entire string, return 0
if startIndex == len(s):
return 0
maxUniqueParts = 0
# Try all possible splits starting at startIndex
for i in range(startIndex, len(s)):
currentPart = s[startIndex:i + 1] # Substring from startIndex to i
if currentPart not in uniqueSet:
uniqueSet.add(currentPart)
# Recursively split the remaining string and count unique parts
maxUniqueParts = max(maxUniqueParts, 1 + self.findMaxUniqueSplit(s, i + 1, uniqueSet))
uniqueSet.remove(currentPart)
return maxUniqueParts
Time Complexity
- Backtracking Exploration:
In the worst case, the function explores all possible ways to split the string
s. The number of ways to split a string of lengthnis bounded by \(2^{n-1}\), since each character can either start a new substring or be part of an existing one. - Checking Substrings:
For each recursive call, the function generates a substring and checks if it exists in the
unordered_set. Generating a substring takes \(O(n)\) time in the worst case, and checking/inserting into the hash set is an average \(O(1)\) operation. - Overall Time Complexity:
Considering the exponential number of splits and the cost of checking/creating substrings, the worst-case time complexity is \(O(n \cdot 2^n)\), where
nis the length of the input string.
Space Complexity
- Recursive Call Stack:
The recursive function uses a call stack with depth proportional to the length of the string, leading to a space complexity of \(O(n)\) for the recursive calls.
- Hash Set Storage:
The
unordered_setstores unique substrings generated during recursion. In the worst case, we can have up to \(n\) unique substrings, leading to \(O(n)\) space for the hash set. - Overall Space Complexity:
The overall space complexity is \(O(n)\), primarily due to the hash set and the recursive call stack.