Maximum Number of Non-Overlapping Substrings
Description:
Given a string s of lowercase letters, you need to find the maximum number of non-overlapping substrings of s that meet the following conditions:
- If a substring contains a certain character
c, it must also contain all occurrences ofcins. - If there are multiple solutions with the maximum number of substrings, return the one with minimum total length.
Return a list of these substrings in any order.
Video Explanation:
Approaches:
1. Intervals and Greedy Scheduling (Optimal)
This problem essentially breaks down into two parts: finding the valid boundaries (intervals) for each unique character, and then selecting the maximum number of non-overlapping intervals (the classic Activity Selection/Interval Scheduling problem).
Algorithm:
- Find Boundaries: First, scan the string to find the first and last occurrence (index) of every character. Store these in two arrays or hash maps,
firstandlast. - Validate Intervals: Iterate through the string. For a character at index
i, we only attempt to construct an interval if it's the very first time we are seeing it (i == first[s[i]]).- Initialize a right boundary as
right = last[s[i]]. - Iterate
jfromitoright. For every character inside this window, check its boundaries. If its first occurrence was beforei, this interval is invalid (we abandon it because a valid substring encompassing both characters would actually start at the earlier index). - If valid, update
right = max(right, last[s[j]])to stretch our substring to cover all occurrences ofs[j]. - If we reach the end of the window without finding an out-of-bounds left character, we have found a valid interval
[i, right].
- Initialize a right boundary as
- Greedy Scheduling: Once all valid intervals are collected, we sort them primarily by their right endpoints in ascending order (to maximize room for subsequent intervals).
- Iterate over the sorted intervals. If an interval starts after our
prev_righttracker, we accept it, add the substring to our result, and updateprev_right.
Complexity
- Time Complexity: . Scanning the string for first/last bounds takes . Validating the intervals also takes overall because the inner loop expands the window, which is bounded by the string length, and at most we process 26 unique starting intervals.
- Space Complexity: or auxiliary space (where is the vocabulary size, at most 26 for lowercase English letters) to store the boundary arrays and valid intervals.
Solutions:
C++
class Solution {
public:
int checkSubstr(const string& s, int i, vector<int>& l, vector<int>& r) {
int right = r[s[i] - 'a'];
for (int j = i; j <= right; ++j) {
if (l[s[j] - 'a'] < i) return -1;
right = max(right, r[s[j] - 'a']);
}
return right;
}
vector<string> maxNumOfSubstrings(string s) {
vector<int> l(26, INT_MAX), r(26, -1);
for (int i = 0; i < s.length(); ++i) {
l[s[i] - 'a'] = min(l[s[i] - 'a'], i);
r[s[i] - 'a'] = max(r[s[i] - 'a'], i);
}
vector<pair<int, int>> intervals;
for (int i = 0; i < s.length(); ++i) {
if (i == l[s[i] - 'a']) {
int right = checkSubstr(s, i, l, r);
if (right != -1) {
intervals.push_back({i, right});
}
}
}
sort(intervals.begin(), intervals.end(), [](const pair<int, int>& a, const pair<int, int>& b) {
if (a.second == b.second) return a.first > b.first;
return a.second < b.second;
});
vector<string> res;
int prevRight = -1;
for (auto& interval : intervals) {
if (interval.first > prevRight) {
res.push_back(s.substr(interval.first, interval.second - interval.first + 1));
prevRight = interval.second;
}
}
return res;
}
};
Java
class Solution {
private int checkSubstr(String s, int i, int[] l, int[] r) {
int right = r[s.charAt(i) - 'a'];
for (int j = i; j <= right; j++) {
if (l[s.charAt(j) - 'a'] < i) return -1;
right = Math.max(right, r[s.charAt(j) - 'a']);
}
return right;
}
public List<String> maxNumOfSubstrings(String s) {
int[] l = new int[26];
int[] r = new int[26];
Arrays.fill(l, Integer.MAX_VALUE);
Arrays.fill(r, -1);
for (int i = 0; i < s.length(); i++) {
l[s.charAt(i) - 'a'] = Math.min(l[s.charAt(i) - 'a'], i);
r[s.charAt(i) - 'a'] = Math.max(r[s.charAt(i) - 'a'], i);
}
List<int[]> intervals = new ArrayList<>();
for (int i = 0; i < s.length(); i++) {
if (i == l[s.charAt(i) - 'a']) {
int right = checkSubstr(s, i, l, r);
if (right != -1) {
intervals.add(new int[]{i, right});
}
}
}
intervals.sort((a, b) -> {
if (a[1] == b[1]) return Integer.compare(b[0], a[0]);
return Integer.compare(a[1], b[1]);
});
List<String> res = new ArrayList<>();
int prevRight = -1;
for (int[] interval : intervals) {
if (interval[0] > prevRight) {
res.add(s.substring(interval[0], interval[1] + 1));
prevRight = interval[1];
}
}
return res;
}
}
Python
class Solution:
def maxNumOfSubstrings(self, s: str) -> list[str]:
first = {c: s.find(c) for c in set(s)}
last = {c: s.rfind(c) for c in set(s)}
def get_valid_right(i):
right = last[s[i]]
j = i
while j <= right:
if first[s[j]] < i:
return -1
right = max(right, last[s[j]])
j += 1
return right
intervals = []
for i in range(len(s)):
if i == first[s[i]]:
right = get_valid_right(i)
if right != -1:
intervals.append((i, right))
intervals.sort(key=lambda x: (x[1], -x[0]))
res = []
prev_right = -1
for left, right in intervals:
if left > prev_right:
res.append(s[left:right + 1])
prev_right = right
return res
JavaScript
/**
* @param {string} s
* @return {string[]}
*/
var maxNumOfSubstrings = function(s) {
const first = new Array(26).fill(Infinity);
const last = new Array(26).fill(-1);
const aCode = 'a'.charCodeAt(0);
for (let i = 0; i < s.length; i++) {
const idx = s.charCodeAt(i) - aCode;
first[idx] = Math.min(first[idx], i);
last[idx] = Math.max(last[idx], i);
}
const checkSubstr = (i) => {
let right = last[s.charCodeAt(i) - aCode];
for (let j = i; j <= right; j++) {
if (first[s.charCodeAt(j) - aCode] < i) return -1;
right = Math.max(right, last[s.charCodeAt(j) - aCode]);
}
return right;
};
const intervals = [];
for (let i = 0; i < s.length; i++) {
const idx = s.charCodeAt(i) - aCode;
if (i === first[idx]) {
const right = checkSubstr(i);
if (right !== -1) {
intervals.push([i, right]);
}
}
}
intervals.sort((a, b) => {
if (a[1] === b[1]) return b[0] - a[0];
return a[1] - b[1];
});
const res = [];
let prevRight = -1;
for (const [l, r] of intervals) {
if (l > prevRight) {
res.push(s.substring(l, r + 1));
prevRight = r;
}
}
return res;
};
Done with this topic? Mark it as complete to track your progress.
Related Practice Problems
Handpicked problems sharing similar algorithmic topic tags
Bigrams
Solution for Codeforces 2242A: Bigrams, utilizing a greedy frequency counting approach.
RemovevomeR
Solution for Codeforces 2241C: RemovevomeR, utilizing a greedy string observation approach.
Valid Parenthesis String
Solving the Valid Parenthesis String problem using an optimal Greedy approach.
💬 Discuss this page
Have a question or spot something confusing in "Maximum Number of Non-Overlapping Substrings"? Ask below — it's backed by GitHub Discussions, so maintainers get notified like any other GitHub activity.