Leetcode 1593. Split a String Into the Max Number of Unique Substrings

Problem Explanation:

The problem is about splitting a given string into a maximum number of unique substrings and counting these unique substrings. A substring can defined as any subset of characters from the original string. While taking substrings from the original string, we need to ensure all resulting substrings are unique and their concatenation results into the given string.

For example, if the string is 'ababc', we can create ['a', 'b', 'ab', 'c'] as unique substrings. Another example is when the given string is 'aa', here we can't split it to more than one unique substrings.

The challenge here is to develop an algorithm that considers all possibilities and chooses the strategy that results into maximum unique substrings.

Solution Approach:

  • We can employ recursive (Depth-first search; DFS) function for this task that goes on creating substrings from the original string until we hit the end.
  • The function starts from an index and creates a substring, if the substring is not already present in our seen substrings set then we move ahead and add it to our set and then make a recursive dfs call now starting from the next index.
  • During backtracking i.e on return from call, we remove this substring from the seen set so that it can be used in another different context.

C++ Solution:

1
2cpp
3class Solution {
4 public:
5  int maxUniqueSplit(string s) {
6    size_t ans = 0;
7    dfs(s, 0, {}, ans);
8    return ans;
9  }
10
11 private:
12  void dfs(const string& s, int start, unordered_set<string>&& seen,
13           size_t& ans) {
14    if (start == s.length()) {
15      ans = max(ans, seen.size());
16      return;
17    }
18
19    for (int i = 1; start + i <= s.length(); ++i) {
20      const string cand = s.substr(start, i);
21      if (seen.count(cand))
22        continue;
23      seen.insert(cand);
24      dfs(s, start + i, move(seen), ans);
25      seen.erase(cand);
26    }
27  }
28};

Java Solution:

1
2java
3class Solution {
4    Set<String> seen = new HashSet<>();
5    int ans = 0;
6
7    public int maxUniqueSplit(String s) {
8        dfs(s, 0);
9        return ans;
10    }
11
12    private void dfs(String s, int start) {
13        if (start == s.length()) {
14            ans = Math.max(ans, seen.size());
15            return;
16        }
17        for (int i = start + 1; i <= s.length(); ++i) {
18            String cand = s.substring(start, i);
19            if (seen.contains(cand)) continue;
20            seen.add(cand);
21            dfs(s, i);
22            seen.remove(cand);
23        }
24    }
25
26}

Python Solution:

1
2python
3class Solution:
4    def maxUniqueSplit(self, s:str) -> int:
5        def dfs(start):
6            if start == len(s):
7                self.ans = max(self.ans, len(seen))
8                return
9            for i in range(start + 1, len(s) + 1):
10                cand = s[start:i]
11                if cand not in seen:
12                    seen.add(cand)
13                    dfs(i)
14                    seen.remove(cand)
15
16        seen, self.ans = set(), 0
17        dfs(0)
18        return self.ans

JavaScript Solution:

1
2javascript
3var maxUniqueSplit = function(s) {
4    let seen = new Set();
5    let ans = 0;
6    
7    function dfs(start) {
8        if (start === s.length) {
9            ans = Math.max(ans, seen.size);
10            return;
11        }
12        
13        for (let i = start + 1; i <= s.length; i++) {
14            let substr = s.slice(start, i);
15            if (seen.has(substr)) continue;
16            seen.add(substr);
17            dfs(i);
18            seen.delete(substr);
19        }
20    }
21    
22    dfs(0);
23    return ans;
24};

C# Solution:

1
2 csharp
3public class Solution {
4    int ans = 0;
5    private HashSet<string> seen = new HashSet<string>();
6    
7    public int MaxUniqueSplit(string s) {
8        dfs(s, 0);
9        return ans;
10    }
11
12    private void dfs(string s, int start) {
13        if(start == s.Length) {
14            ans = Math.Max(ans, seen.Count);
15            return;
16        }
17        
18        for(int i = start + 1; i <= s.Length; i++) {
19            string substring = s.Substring(start, i - start);
20            if(seen.Contains(substring)) {
21                continue;
22            }
23            seen.Add(substring);
24            dfs(s, i);
25            seen.Remove(substring);
26        }
27    }
28}

In all the solutions above, we iterate through the string from left to right, splitting suffixes off and attempting to use them as substrings if they've not been seen before. When we obtain the maximum unique substrings by this method, our answer is stored in ans variable. This is a pattern incorporating depth-first search (DFS) and backtracking, and is a useful approach for many similar string or array splitting problems.


Complexity Analysis:

Time Complexity:

The time complexity for each of the solutions can be considered as O(n!) as we need to explore all possibilities to get all unique substrings. In the worst case, we need to look into each permutation of the string where 'n' is the length of the string.

Space Complexity:

The space complexity for the solutions is O(n) where 'n' is the length of the string. As we are storing n characters in our solutions for a call stack and HashSet, our space complexity is linear.

Conclusion:

In this way, the problem of getting maximum number of unique substrings can be solved using a DFS and backtracking approach. We explored how this can be achieved in multiple languages including Python, Java, JavaScript and C++. With this approach, we efficiently make sure to cover all possible permutations of the string, thus guaranteeing the maximum number of unique substrings. All the solutions are efficient and have a time complexity of O(n!) and space complexity of O(n).


Got a question? Ask the Teaching Assistant anything you don't understand.

Still not clear? Ask in the Forum,  Discord or Submit the part you don't understand to our editors.


TA 👨‍🏫