1234. Replace the Substring for Balanced String
Problem Description
You have a string s
that contains only four types of characters: 'Q'
, 'W'
, 'E'
, and 'R'
. The length of this string is n
.
A string is considered balanced when each of the four characters appears exactly n/4
times in the string.
Your task is to find the minimum length of a substring that you can replace with any other characters to make the entire string balanced. You can replace the substring with any characters you want, as long as the replacement has the same length as the original substring.
If the string is already balanced, return 0
.
For example:
- If
s = "QWER"
with length 4, it's already balanced since each character appears exactly 1 time (4/4 = 1), so return0
- If
s = "QQWE"
with length 4, it's not balanced. You could replace one'Q'
to get a balanced string, so the minimum substring length to replace would be1
- If
s = "QQQWWWEEEERRRR"
with length 14, it's not balanced since we need each character to appear 14/4 = 3.5 times (but since this must be an integer, we need each to appear 3 times when n=12)
The key insight is that you need to find the shortest contiguous substring that, when replaced appropriately, would result in each of the four characters appearing exactly n/4
times in the entire string.
Intuition
Let's think about what makes a string balanced - each character must appear exactly n/4
times. If we have too many of some characters and too few of others, we need to replace the excess characters with the missing ones.
The key insight is that we need to find a substring that contains all the "excess" characters. Once we replace this substring, we can put whatever characters we need to achieve balance.
For example, if we need each character to appear 3 times but we have Q: 5, W: 2, E: 3, R: 2
, then Q
has 2 excess occurrences. We need to find the shortest substring containing at least 2 Q
s - we can then replace those with the missing W
and R
.
This naturally leads to a sliding window approach. We want to find the minimum window that, when removed (and replaced), leaves us with at most n/4
of each character in the remaining part of the string.
Think of it this way: instead of tracking what's inside the window to replace, we track what's outside the window. The characters outside the window are what will remain after we replace the window's content. For the string to be balanced after replacement, the count of each character outside the window must not exceed n/4
.
We can use two pointers to maintain this window:
- Expand the window by moving the right pointer, which removes characters from the "outside" count
- When the outside satisfies our condition (all counts ≤
n/4
), try to shrink the window from the left to find the minimum size - Keep track of the minimum valid window size found
This approach works because once we find a valid window (where the outside has ≤ n/4
of each character), we can always replace the window's content with the exact characters needed to achieve perfect balance.
Learn more about Sliding Window patterns.
Solution Approach
We implement the sliding window approach using two pointers to find the minimum substring that needs to be replaced.
Step 1: Initial Count and Early Exit
First, we count the frequency of each character using a Counter (hash table):
cnt = Counter(s)
n = len(s)
If all characters already appear at most n/4
times, the string is already balanced:
if all(v <= n // 4 for v in cnt.values()):
return 0
Step 2: Sliding Window Setup
Initialize variables for the answer and left pointer:
ans, j = n, 0
ans
starts atn
(worst case: replace the entire string)j
is the left boundary of our window (starts at 0)
Step 3: Expand and Contract Window
Iterate through the string with the right pointer i
:
for i, c in enumerate(s):
cnt[c] -= 1 # Remove current character from "outside" count
When we include a character in our window (at position i
), we decrease its count in cnt
. Now cnt
represents the frequency of characters outside our window.
Step 4: Try to Shrink Window
While the characters outside the window satisfy our condition (all counts ≤ n/4
):
while j <= i and all(v <= n // 4 for v in cnt.values()):
ans = min(ans, i - j + 1) # Update minimum window size
cnt[s[j]] += 1 # Add character back to "outside" count
j += 1 # Shrink window from left
This inner loop:
- Checks if removing the window
[j, i]
leaves valid counts outside - Updates the minimum window size if valid
- Moves the left boundary right to find an even smaller valid window
- Adds the character at position
j
back to the outside count before movingj
Step 5: Return Result
The algorithm continues until we've considered all possible windows, then returns the minimum size found:
return ans
Time Complexity: O(n)
where n
is the length of the string. Although we have nested loops, each character is visited at most twice (once by each pointer).
Space Complexity: O(1)
since we only track counts for at most 4 different 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 = "QQQWWWEEEERRRR"
(length 14).
Step 1: Initial Setup
n = 14
, so we need each character to appearn/4 = 14/4 = 3
times (integer division)- Count frequencies:
Q: 3, W: 3, E: 4, R: 4
- Since
E
andR
both appear 4 times (more than 3), the string is not balanced
Step 2: Identify the Problem
- We have 1 excess
E
and 1 excessR
- We need to find the shortest substring containing at least 1
E
and 1R
to replace
Step 3: Sliding Window Process
Initialize: ans = 14
, j = 0
, cnt = {Q: 3, W: 3, E: 4, R: 4}
Window [0, 9]: "QQQWWWEEEE"
- As we expand right pointer
i
from 0 to 9, we remove characters from outside count:- After including positions 0-2:
cnt = {Q: 0, W: 3, E: 4, R: 4}
- After including positions 3-5:
cnt = {Q: 0, W: 0, E: 4, R: 4}
- After including positions 6-9:
cnt = {Q: 0, W: 0, E: 0, R: 4}
- After including positions 0-2:
- Outside count invalid (R: 4 > 3), so we can't shrink yet
Window [0, 10]: "QQQWWWEEEЕР"
- Include position 10 ('R'):
cnt = {Q: 0, W: 0, E: 0, R: 3}
- Now all counts ≤ 3! Valid window found
- Try shrinking from left:
- Current window size: 11
- Update
ans = min(14, 11) = 11
- Add s[0]='Q' back:
cnt = {Q: 1, W: 0, E: 0, R: 3}
- Move
j
to 1
Window [1, 10]: "QQWWWEEEЕР"
- Still valid (all counts ≤ 3)
- Window size: 10
- Update
ans = min(11, 10) = 10
- Continue shrinking...
Continue this process, trying smaller windows like [7, 11]: "EEEЕР" (size 5)
- Outside this window:
cnt = {Q: 3, W: 3, E: 0, R: 3}
- All counts ≤ 3, so valid!
- Update
ans = min(previous, 5) = 5
Final Answer: 5
The shortest substring we need to replace is length 5 (e.g., "EEEЕР"). We can replace it with "QWEER" or any combination that balances the string.
Solution Implementation
1from collections import Counter
2
3class Solution:
4 def balancedString(self, s: str) -> int:
5 # Count frequency of each character in the string
6 char_count = Counter(s)
7 n = len(s)
8 target_count = n // 4 # Each character should appear exactly n/4 times in a balanced string
9
10 # If string is already balanced, no replacement needed
11 if all(count <= target_count for count in char_count.values()):
12 return 0
13
14 min_window_size = n # Initialize minimum window size to string length
15 left = 0 # Left pointer of sliding window
16
17 # Iterate through string with right pointer
18 for right, char in enumerate(s):
19 # Remove current character from count (considering it part of window to replace)
20 char_count[char] -= 1
21
22 # Try to shrink window from left while maintaining valid state
23 # Valid state: remaining characters outside window don't exceed target_count
24 while left <= right and all(count <= target_count for count in char_count.values()):
25 # Update minimum window size
26 min_window_size = min(min_window_size, right - left + 1)
27 # Add back the leftmost character (removing it from replacement window)
28 char_count[s[left]] += 1
29 left += 1
30
31 return min_window_size
32
1class Solution {
2 public int balancedString(String s) {
3 // Initialize frequency array for 4 characters (Q, W, E, R)
4 int[] charCount = new int[4];
5 String characters = "QWER";
6 int n = s.length();
7
8 // Count frequency of each character in the string
9 for (int i = 0; i < n; i++) {
10 charCount[characters.indexOf(s.charAt(i))]++;
11 }
12
13 // Calculate the target count for each character in a balanced string
14 int targetCount = n / 4;
15
16 // Check if string is already balanced
17 if (charCount[0] == targetCount && charCount[1] == targetCount &&
18 charCount[2] == targetCount && charCount[3] == targetCount) {
19 return 0;
20 }
21
22 // Use sliding window to find minimum substring to replace
23 int minLength = n;
24 int left = 0;
25
26 // Iterate through string with right pointer
27 for (int right = 0; right < n; right++) {
28 // Remove current character from count (include it in window)
29 charCount[characters.indexOf(s.charAt(right))]--;
30
31 // Try to shrink window from left while maintaining valid condition
32 // Valid condition: all characters outside window have count <= targetCount
33 while (left <= right &&
34 charCount[0] <= targetCount && charCount[1] <= targetCount &&
35 charCount[2] <= targetCount && charCount[3] <= targetCount) {
36 // Update minimum window length
37 minLength = Math.min(minLength, right - left + 1);
38
39 // Add character at left back to count (exclude from window) and move left pointer
40 charCount[characters.indexOf(s.charAt(left))]++;
41 left++;
42 }
43 }
44
45 return minLength;
46 }
47}
48
1class Solution {
2public:
3 int balancedString(string s) {
4 // Array to store frequency count of each character
5 // Index 0: 'Q', Index 1: 'W', Index 2: 'E', Index 3: 'R'
6 int charCount[4] = {0};
7 string charMapping = "QWER";
8 int stringLength = s.size();
9
10 // Count frequency of each character in the string
11 for (char& ch : s) {
12 charCount[charMapping.find(ch)]++;
13 }
14
15 // Calculate the target count for each character in a balanced string
16 int targetCount = stringLength / 4;
17
18 // If string is already balanced, return 0
19 if (charCount[0] == targetCount && charCount[1] == targetCount &&
20 charCount[2] == targetCount && charCount[3] == targetCount) {
21 return 0;
22 }
23
24 // Use sliding window to find minimum substring to replace
25 int minLength = stringLength;
26 int left = 0;
27
28 // Expand window with right pointer
29 for (int right = 0; right < stringLength; ++right) {
30 // Remove current character from count (considering it for replacement)
31 charCount[charMapping.find(s[right])]--;
32
33 // Try to shrink window from left while maintaining valid condition
34 // Valid: all character counts are <= targetCount (excess can be replaced)
35 while (left <= right &&
36 charCount[0] <= targetCount && charCount[1] <= targetCount &&
37 charCount[2] <= targetCount && charCount[3] <= targetCount) {
38
39 // Update minimum length found
40 minLength = min(minLength, right - left + 1);
41
42 // Add back the character at left pointer and move left pointer
43 charCount[charMapping.find(s[left])]++;
44 left++;
45 }
46 }
47
48 return minLength;
49 }
50};
51
1function balancedString(s: string): number {
2 // Array to store frequency count of each character
3 // Index 0: 'Q', Index 1: 'W', Index 2: 'E', Index 3: 'R'
4 const charCount: number[] = [0, 0, 0, 0];
5 const charMapping: string = "QWER";
6 const stringLength: number = s.length;
7
8 // Count frequency of each character in the string
9 for (let i = 0; i < stringLength; i++) {
10 const charIndex: number = charMapping.indexOf(s[i]);
11 charCount[charIndex]++;
12 }
13
14 // Calculate the target count for each character in a balanced string
15 const targetCount: number = stringLength / 4;
16
17 // If string is already balanced, return 0
18 if (charCount[0] === targetCount && charCount[1] === targetCount &&
19 charCount[2] === targetCount && charCount[3] === targetCount) {
20 return 0;
21 }
22
23 // Use sliding window to find minimum substring to replace
24 let minLength: number = stringLength;
25 let left: number = 0;
26
27 // Expand window with right pointer
28 for (let right = 0; right < stringLength; right++) {
29 // Remove current character from count (considering it for replacement)
30 const rightCharIndex: number = charMapping.indexOf(s[right]);
31 charCount[rightCharIndex]--;
32
33 // Try to shrink window from left while maintaining valid condition
34 // Valid: all character counts are <= targetCount (excess can be replaced)
35 while (left <= right &&
36 charCount[0] <= targetCount && charCount[1] <= targetCount &&
37 charCount[2] <= targetCount && charCount[3] <= targetCount) {
38
39 // Update minimum length found
40 minLength = Math.min(minLength, right - left + 1);
41
42 // Add back the character at left pointer and move left pointer
43 const leftCharIndex: number = charMapping.indexOf(s[left]);
44 charCount[leftCharIndex]++;
45 left++;
46 }
47 }
48
49 return minLength;
50}
51
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the string s
. Although there's a nested loop structure with the outer for
loop and inner while
loop, each character in the string is visited at most twice - once by the pointer i
(moving right) and once by the pointer j
(moving left). The pointer j
only moves forward and never resets, making this a two-pointer sliding window approach. The all()
function inside the loops checks at most 4 values (for characters 'Q', 'W', 'E', 'R'), which is a constant operation O(1)
.
The space complexity is O(C)
, where C
is the size of the character set. In this problem, C = 4
since we only deal with the characters 'Q', 'W', 'E', and 'R'. The Counter
object cnt
stores at most 4 key-value pairs, making the space requirement constant O(4) = O(1)
. However, following the reference answer's notation, we express it as O(C)
to be more general about the character set size.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Misunderstanding What the Counter Represents
Pitfall: A common mistake is misunderstanding what char_count
represents after we start the sliding window. Initially, it counts all characters in the string, but once we start moving the window, it represents the count of characters outside the window (not inside).
Why this happens: When we do char_count[char] -= 1
at position right
, we're conceptually "removing" that character from the outside count because it's now part of our replacement window.
Example of the confusion:
# WRONG interpretation: # "char_count tracks what's inside the window" # CORRECT interpretation: # "char_count tracks what's outside the window (what remains after replacement)"
2. Incorrect Validation Check
Pitfall: Checking if the window itself is valid rather than checking if what's outside the window is valid.
Wrong approach:
# INCORRECT: Checking if window contents are balanced
window_count = Counter(s[left:right+1])
if all(count == target_count for count in window_count.values()):
# This doesn't help us determine if replacing this window fixes the string
Correct approach:
# CORRECT: Check if remaining characters (outside window) don't exceed target
if all(count <= target_count for count in char_count.values()):
# If true, we can replace the window to balance the string
3. Off-by-One Error in Window Size Calculation
Pitfall: Incorrectly calculating the window size as right - left
instead of right - left + 1
.
Wrong:
min_window_size = min(min_window_size, right - left) # Missing the +1
Correct:
min_window_size = min(min_window_size, right - left + 1) # Inclusive range
4. Forgetting to Handle Edge Cases
Pitfall: Not handling strings where n
is not divisible by 4.
Issue: The problem assumes n
is divisible by 4 (since we need each character to appear exactly n/4
times). If this isn't guaranteed, you need to add validation:
def balancedString(self, s: str) -> int:
n = len(s)
# Add validation if needed
if n % 4 != 0:
return -1 # Or handle according to problem requirements
target_count = n // 4
# ... rest of the code
5. Premature Window Shrinking
Pitfall: Moving the left pointer before checking if the current window is valid.
Wrong order:
while left <= right:
left += 1 # Moving left first
char_count[s[left-1]] += 1
if all(count <= target_count for count in char_count.values()):
min_window_size = min(min_window_size, right - left + 1)
Correct order:
while left <= right and all(count <= target_count for count in char_count.values()):
min_window_size = min(min_window_size, right - left + 1) # Update first
char_count[s[left]] += 1 # Then move pointer
left += 1
The correct order ensures we capture the minimum valid window before shrinking it further.
Which of the following uses divide and conquer strategy?
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!