3090. Maximum Length Substring With Two Occurrences
Problem Description
Given a string s
, you need to find the length of the longest substring where each character appears at most twice.
In other words, you're looking for the maximum length of a continuous portion of the string where no character occurs more than 2 times within that portion.
For example:
- If
s = "bcbbbcba"
, a valid substring could be"bcbbbcb"
where 'b' appears twice (at positions relative to this substring), 'c' appears twice, but if we tried to extend it further to include the final 'a', we'd still be valid. However, the substring"bcbbbc"
has 'b' appearing 4 times, which exceeds the limit of 2. - The goal is to find the longest such valid substring and return its length.
The key constraint is that within your chosen substring, every unique character must appear at most 2 times. You want to maximize the length of such a substring.
Intuition
When we need to find the longest substring with certain constraints, the sliding window technique naturally comes to mind. The key insight is that we can maintain a window that always satisfies our constraint (each character appears at most twice).
Think of it like a flexible window that we can expand and shrink as needed. We start with an empty window and gradually expand it by moving the right boundary. As we add each new character, we keep track of how many times each character appears in our current window.
The critical moment comes when adding a new character would violate our constraint - that is, when a character would appear more than twice. At this point, we can't simply skip the character because we need a continuous substring. Instead, we must shrink our window from the left side.
Here's the clever part: we don't need to shrink the window completely and start over. We only need to shrink it just enough to make it valid again. Since we just added a character that caused some character c
to appear more than twice, we move the left boundary rightward, removing characters one by one, until the count of c
drops back to 2.
This approach ensures that at every step, we maintain the largest valid window possible ending at the current position. By keeping track of the maximum window size we've seen throughout this process, we find our answer.
The beauty of this solution is its efficiency - we visit each character at most twice (once when expanding the window, once when shrinking it), giving us a linear time complexity. We also only need to track character counts, which requires minimal extra space.
Learn more about Sliding Window patterns.
Solution Approach
We implement the sliding window technique using two pointers to maintain a valid substring. Here's how the algorithm works:
Data Structures:
- A counter/hash map
cnt
to track the frequency of each character in the current window - Two pointers:
i
(left boundary) andj
(right boundary) of the sliding window - A variable
ans
to store the maximum length found so far
Algorithm Steps:
-
Initialize the window: Start with
i = 0
as the left pointer, andans = 0
for tracking the maximum length. The countercnt
starts empty. -
Expand the window: Iterate through the string with index
j
and characterc
:- Add the character to our window by incrementing
cnt[c]
- Add the character to our window by incrementing
-
Maintain the constraint: After adding each character, check if
cnt[c] > 2
:- If yes, we need to shrink the window from the left
- Keep moving the left pointer
i
rightward, decreasing the count of each character we remove (cnt[s[i]] -= 1
) - Continue until
cnt[c] <= 2
(the window becomes valid again)
-
Update the maximum: After ensuring the window is valid, calculate the current window size as
j - i + 1
and updateans
if this is larger than the previous maximum. -
Return the result: After processing all characters,
ans
contains the maximum length of a valid substring.
Example walkthrough:
For string s = "bcbbbcba"
:
- When we reach the 5th 'b', we have too many 'b's in our window
- We shrink from the left, removing characters until we have at most 2 'b's
- Throughout this process, we track the maximum valid window size
The time complexity is O(n)
where n
is the length of the string, since each character is visited at most twice (once by each pointer). The space complexity is O(k)
where k
is the size of the character set (at most 26 for lowercase letters or 128 for ASCII characters).
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with s = "eceba"
:
Initial State:
- Left pointer
i = 0
, right pointerj = 0
- Counter
cnt = {}
(empty) - Maximum length
ans = 0
Step 1: j = 0, character = 'e'
- Add 'e' to window:
cnt = {'e': 1}
- Check constraint: 'e' appears 1 time ≤ 2 ✓
- Current window: "e" (from index 0 to 0)
- Window size: 0 - 0 + 1 = 1
- Update
ans = 1
Step 2: j = 1, character = 'c'
- Add 'c' to window:
cnt = {'e': 1, 'c': 1}
- Check constraint: 'c' appears 1 time ≤ 2 ✓
- Current window: "ec" (from index 0 to 1)
- Window size: 1 - 0 + 1 = 2
- Update
ans = 2
Step 3: j = 2, character = 'e'
- Add 'e' to window:
cnt = {'e': 2, 'c': 1}
- Check constraint: 'e' appears 2 times ≤ 2 ✓
- Current window: "ece" (from index 0 to 2)
- Window size: 2 - 0 + 1 = 3
- Update
ans = 3
Step 4: j = 3, character = 'b'
- Add 'b' to window:
cnt = {'e': 2, 'c': 1, 'b': 1}
- Check constraint: 'b' appears 1 time ≤ 2 ✓
- Current window: "eceb" (from index 0 to 3)
- Window size: 3 - 0 + 1 = 4
- Update
ans = 4
Step 5: j = 4, character = 'a'
- Add 'a' to window:
cnt = {'e': 2, 'c': 1, 'b': 1, 'a': 1}
- Check constraint: 'a' appears 1 time ≤ 2 ✓
- Current window: "eceba" (from index 0 to 4)
- Window size: 4 - 0 + 1 = 5
- Update
ans = 5
Result: The longest substring where each character appears at most twice is the entire string "eceba" with length 5.
Let's also trace through a case where we need to shrink the window: s = "aaabb"
:
Steps 1-4: Build window "aaab" with cnt = {'a': 3, 'b': 1}
- After adding the 3rd 'a' at j=2:
cnt['a'] = 3 > 2
❌ - Shrink: Remove s[0]='a', move i to 1:
cnt = {'a': 2, 'b': 0}
- Now valid again with window "aa" (size = 2)
Step 5: j = 4, character = 'b'
- Add second 'b':
cnt = {'a': 2, 'b': 2}
- Current window: "aabb" (from index 1 to 4)
- Window size: 4 - 1 + 1 = 4
- Final
ans = 4
The algorithm efficiently maintains the largest valid window by only shrinking when necessary and only as much as needed.
Solution Implementation
1from collections import Counter
2
3class Solution:
4 def maximumLengthSubstring(self, s: str) -> int:
5 # Dictionary to track character frequencies in current window
6 char_count = Counter()
7
8 # Initialize result and left pointer of sliding window
9 max_length = 0
10 left = 0
11
12 # Iterate through string with right pointer
13 for right, char in enumerate(s):
14 # Add current character to window
15 char_count[char] += 1
16
17 # Shrink window from left while any character appears more than twice
18 while char_count[char] > 2:
19 char_count[s[left]] -= 1
20 left += 1
21
22 # Update maximum length found so far
23 # Window size is right - left + 1
24 max_length = max(max_length, right - left + 1)
25
26 return max_length
27
1class Solution {
2 public int maximumLengthSubstring(String s) {
3 // Array to track frequency of each character (a-z)
4 int[] charFrequency = new int[26];
5 int maxLength = 0;
6
7 // Use sliding window technique with two pointers
8 int left = 0;
9 for (int right = 0; right < s.length(); right++) {
10 // Get the index of current character (0-25 for a-z)
11 int currentCharIndex = s.charAt(right) - 'a';
12
13 // Increment frequency of current character
14 charFrequency[currentCharIndex]++;
15
16 // If any character appears more than 2 times, shrink window from left
17 while (charFrequency[currentCharIndex] > 2) {
18 int leftCharIndex = s.charAt(left) - 'a';
19 charFrequency[leftCharIndex]--;
20 left++;
21 }
22
23 // Update maximum length found so far
24 // Window size is (right - left + 1)
25 maxLength = Math.max(maxLength, right - left + 1);
26 }
27
28 return maxLength;
29 }
30}
31
1class Solution {
2public:
3 int maximumLengthSubstring(string s) {
4 // Array to count frequency of each character (a-z)
5 int charCount[26] = {0};
6
7 // Variable to store the maximum length found
8 int maxLength = 0;
9
10 // Sliding window approach with left and right pointers
11 int left = 0;
12 for (int right = 0; right < s.length(); ++right) {
13 // Get the index of current character (0-25 for a-z)
14 int currentCharIndex = s[right] - 'a';
15
16 // Increment count for current character
17 ++charCount[currentCharIndex];
18
19 // Shrink window from left if any character appears more than twice
20 while (charCount[currentCharIndex] > 2) {
21 int leftCharIndex = s[left] - 'a';
22 --charCount[leftCharIndex];
23 ++left;
24 }
25
26 // Update maximum length with current window size
27 int currentWindowSize = right - left + 1;
28 maxLength = max(maxLength, currentWindowSize);
29 }
30
31 return maxLength;
32 }
33};
34
1/**
2 * Finds the maximum length of a substring where each character appears at most twice
3 * @param s - The input string containing lowercase English letters
4 * @returns The maximum length of a valid substring
5 */
6function maximumLengthSubstring(s: string): number {
7 // Variable to store the maximum length found
8 let maxLength: number = 0;
9
10 // Array to count occurrences of each character (26 lowercase letters)
11 const charCount: number[] = Array(26).fill(0);
12
13 // Sliding window approach with left and right pointers
14 let leftPointer: number = 0;
15
16 for (let rightPointer: number = 0; rightPointer < s.length; rightPointer++) {
17 // Calculate the index for the current character (0-25 for 'a'-'z')
18 const currentCharIndex: number = s.charCodeAt(rightPointer) - 'a'.charCodeAt(0);
19
20 // Increment the count for the current character
21 charCount[currentCharIndex]++;
22
23 // Shrink the window from the left if any character appears more than twice
24 while (charCount[currentCharIndex] > 2) {
25 const leftCharIndex: number = s.charCodeAt(leftPointer) - 'a'.charCodeAt(0);
26 charCount[leftCharIndex]--;
27 leftPointer++;
28 }
29
30 // Update the maximum length with the current window size
31 const currentWindowSize: number = rightPointer - leftPointer + 1;
32 maxLength = Math.max(maxLength, currentWindowSize);
33 }
34
35 return maxLength;
36}
37
Time and Space Complexity
Time Complexity: O(n)
, where n
is the length of the string s
.
The algorithm uses a two-pointer sliding window approach. The outer loop iterates through each character in the string exactly once using the j
pointer. The inner while
loop moves the i
pointer forward only when needed. Each character is visited at most twice - once by the j
pointer when expanding the window and at most once by the i
pointer when shrinking the window. Therefore, the total number of operations is bounded by 2n
, which simplifies to O(n)
.
Space Complexity: O(|Σ|)
, where Σ
is the character set.
The space is used by the Counter
object cnt
which stores the frequency of characters in the current window. In the worst case, the counter will contain all unique characters that appear in the string. Since the problem deals with lowercase English letters (based on the constraint that each character can appear at most 2 times in a valid substring), the character set size |Σ| = 26
. Therefore, the space complexity is O(26)
which simplifies to O(1)
constant space, or more precisely O(|Σ|)
where Σ = 26
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Inefficient Window Shrinking
The Problem: A common mistake is shrinking the window more than necessary or using a nested while loop that checks all characters in the window, not just the problematic one.
Incorrect approach:
# Wrong: Checking all characters unnecessarily
while any(count > 2 for count in char_count.values()):
char_count[s[left]] -= 1
if char_count[s[left]] == 0:
del char_count[s[left]]
left += 1
Why it's wrong: This checks every character's count in each iteration, leading to O(k) operations per shrink where k is the number of unique characters. This could degrade performance to O(n*k) overall.
Correct approach:
# Right: Only check the character that just exceeded the limit while char_count[char] > 2: char_count[s[left]] -= 1 left += 1
Pitfall 2: Forgetting to Clean Up Zero Counts
The Problem: Not removing characters with zero count from the dictionary can lead to memory inefficiency in cases with many unique characters.
Better implementation:
while char_count[char] > 2: char_count[s[left]] -= 1 if char_count[s[left]] == 0: del char_count[s[left]] # Clean up to save memory left += 1
Pitfall 3: Off-by-One Error in Window Size Calculation
The Problem: Incorrectly calculating the window size as right - left
instead of right - left + 1
.
Wrong:
max_length = max(max_length, right - left) # Missing the +1
Correct:
max_length = max(max_length, right - left + 1) # Includes both endpoints
Pitfall 4: Updating Maximum Length at Wrong Time
The Problem: Updating the maximum length before ensuring the window is valid.
Wrong:
for right, char in enumerate(s):
char_count[char] += 1
max_length = max(max_length, right - left + 1) # Window might be invalid!
while char_count[char] > 2:
char_count[s[left]] -= 1
left += 1
Correct:
for right, char in enumerate(s):
char_count[char] += 1
while char_count[char] > 2:
char_count[s[left]] -= 1
left += 1
max_length = max(max_length, right - left + 1) # Window is now valid
Pitfall 5: Using Fixed-Size Array Instead of Dictionary
The Problem: Using a fixed-size array for character counting when the character set is unknown or large.
Potentially problematic:
# Assumes only lowercase letters
char_count = [0] * 26
char_count[ord(char) - ord('a')] += 1
More robust:
# Works for any character set char_count = Counter() char_count[char] += 1
The dictionary approach is more flexible and handles any Unicode characters without modification, while the array approach requires knowing the exact character set beforehand.
Which algorithm is best for finding the shortest distance between two points in an unweighted graph?
Recommended Readings
https assets algo monster cover_photos stack svg Sliding Window Maximum Monotonic Stack We have an array and a sliding window defined by a start index and an end index The sliding window moves from left of the array to right There are always k elements in the window The window
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!