1653. Minimum Deletions to Make String Balanced
Problem Description
You have a string s
that contains only characters 'a'
and 'b'
.
A string is considered balanced when there are no positions where a 'b'
appears before an 'a'
. In other words, for any pair of indices (i,j)
where i < j
, if s[i] = 'b'
and s[j] = 'a'
, the string is not balanced.
You can delete any number of characters from s
to make it balanced. Your task is to find the minimum number of deletions needed to achieve this.
For example:
- The string
"aababbab"
is not balanced because there are'b'
characters that appear before'a'
characters - The string
"bbbbb"
is balanced (all'b'
s, no'a'
after any'b'
) - The string
"aaaa"
is balanced (all'a'
s) - The string
"aaabbb"
is balanced (all'a'
s come before all'b'
s)
The goal is to remove the fewest characters possible to ensure no 'b'
comes before any 'a'
in the resulting string.
Intuition
To make the string balanced, we need to ensure no 'b'
appears before any 'a'
. Let's think about what happens as we process the string from left to right.
At each position, we need to make a decision based on the current character:
-
If we encounter a
'b'
: This doesn't create any immediate problem. The string up to this point remains as balanced as it was before, since'b'
can appear at the end without issues. -
If we encounter an
'a'
: This is where we face a choice. Any'b'
that appeared before this'a'
creates an imbalance. We have two options:- Delete this
'a'
to avoid the conflict (cost: 1 deletion) - Keep this
'a'
but delete all previous'b'
s (cost: number of'b'
s seen so far)
- Delete this
The key insight is that we want to choose the option with minimum cost at each step.
This naturally leads to a dynamic programming approach where:
- We track the minimum deletions needed to balance the string up to each position
- We maintain a count of
'b'
s seen so far - For each
'a'
, we choose the minimum between deleting it or deleting all previous'b'
s
The formula becomes:
- When we see a
'b'
:f[i] = f[i-1]
(no additional deletions needed) - When we see an
'a'
:f[i] = min(f[i-1] + 1, b)
whereb
is the count of'b'
s seen so far
This greedy choice at each step ensures we find the globally optimal solution with minimum deletions.
Learn more about Stack and Dynamic Programming patterns.
Solution Approach
The solution uses dynamic programming to track the minimum deletions needed at each position.
State Definition:
f[i]
represents the minimum number of deletions needed to make the firsti
characters balancedb
counts the number of'b'
characters seen so far
Initialization:
f[0] = 0
(empty string is balanced)b = 0
(no'b'
s seen initially)
State Transition:
We iterate through the string with index i
from 1 to n
:
-
When
s[i-1] = 'b'
:- The current
'b'
doesn't create imbalance f[i] = f[i-1]
(no additional deletions)- Update
b += 1
to track this'b'
- The current
-
When
s[i-1] = 'a'
:- Option 1: Delete this
'a'
→ cost isf[i-1] + 1
- Option 2: Keep this
'a'
and delete all previous'b'
s → cost isb
f[i] = min(f[i-1] + 1, b)
- Option 1: Delete this
The state transition equation is:
f[i] = { f[i-1], if s[i-1] = 'b' min(f[i-1] + 1, b), if s[i-1] = 'a' }
Implementation Details:
def minimumDeletions(self, s: str) -> int:
n = len(s)
f = [0] * (n + 1) # DP array
b = 0 # Count of 'b's
for i, c in enumerate(s, 1): # 1-indexed iteration
if c == 'b':
f[i] = f[i - 1]
b += 1
else: # c == 'a'
f[i] = min(f[i - 1] + 1, b)
return f[n]
Space Optimization:
Since each state only depends on the previous state and variable b
, we could optimize space to O(1) by using a single variable instead of an array, though the given solution uses O(n) space for clarity.
Time Complexity: O(n) - single pass through the string Space Complexity: O(n) - for the DP array
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 trace through the string s = "aababbab"
to see how the algorithm finds the minimum deletions needed.
Initial Setup:
- String:
"aababbab"
(length = 8) - DP array
f = [0, 0, 0, 0, 0, 0, 0, 0, 0]
(size 9, initialized to 0) - Counter
b = 0
(tracks number of 'b's seen)
Step-by-step Processing:
Step | i | char | b before | Decision | f[i] calculation | f[i] | b after |
---|---|---|---|---|---|---|---|
Init | 0 | - | 0 | - | - | 0 | 0 |
1 | 1 | 'a' | 0 | Delete 'a' or delete 0 'b's | min(0+1, 0) | 0 | 0 |
2 | 2 | 'a' | 0 | Delete 'a' or delete 0 'b's | min(0+1, 0) | 0 | 0 |
3 | 3 | 'b' | 0 | Keep 'b' | f[2] | 0 | 1 |
4 | 4 | 'a' | 1 | Delete 'a' or delete 1 'b' | min(0+1, 1) | 1 | 1 |
5 | 5 | 'b' | 1 | Keep 'b' | f[4] | 1 | 2 |
6 | 6 | 'b' | 2 | Keep 'b' | f[5] | 1 | 3 |
7 | 7 | 'a' | 3 | Delete 'a' or delete 3 'b's | min(1+1, 3) | 2 | 3 |
8 | 8 | 'b' | 3 | Keep 'b' | f[7] | 2 | 4 |
Detailed Decisions:
- Position 1-2 ('a', 'a'): No 'b's seen yet, so keeping these 'a's costs nothing
- Position 3 ('b'): First 'b' appears, no conflict yet
- Position 4 ('a'): This 'a' comes after 1 'b'. Either delete this 'a' (cost: 1) or delete the previous 'b' (cost: 1). Both equal, choose minimum = 1
- Position 5-6 ('b', 'b'): More 'b's accumulate, no new conflicts
- Position 7 ('a'): This 'a' comes after 3 'b's. Either delete this 'a' (cost: 1+1=2) or delete all 3 previous 'b's (cost: 3). Choose minimum = 2
- Position 8 ('b'): Final 'b', no new conflict
Result: The minimum number of deletions needed is f[8] = 2
Verification: One optimal solution is to delete the two 'a's at positions 4 and 7, leaving "aabbbb"
which is balanced (all 'a's before all 'b's).
Solution Implementation
1class Solution:
2 def minimumDeletions(self, s: str) -> int:
3 # Get the length of the input string
4 n = len(s)
5
6 # dp[i] represents minimum deletions needed to make s[0:i] balanced
7 # We use n+1 size to handle 1-indexed iteration
8 dp = [0] * (n + 1)
9
10 # Count of 'b' characters seen so far
11 b_count = 0
12
13 # Iterate through each character with 1-based indexing
14 for i, char in enumerate(s, 1):
15 if char == 'b':
16 # If current character is 'b', no additional deletion needed
17 # The string remains balanced up to this point
18 dp[i] = dp[i - 1]
19 b_count += 1
20 else:
21 # If current character is 'a', we have two choices:
22 # 1. Delete this 'a' (cost: dp[i-1] + 1)
23 # 2. Keep this 'a' and delete all previous 'b's (cost: b_count)
24 dp[i] = min(dp[i - 1] + 1, b_count)
25
26 # Return the minimum deletions needed for the entire string
27 return dp[n]
28
1class Solution {
2 public int minimumDeletions(String s) {
3 int length = s.length();
4
5 // dp[i] represents the minimum deletions needed to make s[0..i-1] valid
6 // A valid string has all 'a's before all 'b's
7 int[] dp = new int[length + 1];
8
9 // Count of 'b's encountered so far
10 int bCount = 0;
11
12 // Iterate through each character in the string
13 for (int i = 1; i <= length; i++) {
14 char currentChar = s.charAt(i - 1);
15
16 if (currentChar == 'b') {
17 // If current character is 'b', no need to delete it
18 // The minimum deletions remain the same as previous state
19 dp[i] = dp[i - 1];
20 bCount++;
21 } else {
22 // If current character is 'a', we have two choices:
23 // 1. Delete this 'a' (cost: dp[i-1] + 1)
24 // 2. Keep this 'a' and delete all previous 'b's (cost: bCount)
25 dp[i] = Math.min(dp[i - 1] + 1, bCount);
26 }
27 }
28
29 // Return the minimum deletions needed for the entire string
30 return dp[length];
31 }
32}
33
1class Solution {
2public:
3 int minimumDeletions(string s) {
4 int stringLength = s.size();
5
6 // dp[i] represents the minimum deletions needed to make s[0...i-1] balanced
7 // where balanced means no 'b' appears before any 'a'
8 vector<int> dp(stringLength + 1, 0);
9
10 // Count of 'b' characters seen so far
11 int bCount = 0;
12
13 // Iterate through each character in the string
14 for (int i = 1; i <= stringLength; ++i) {
15 if (s[i - 1] == 'b') {
16 // If current character is 'b', it doesn't violate the balance
17 // So minimum deletions remain the same as previous state
18 dp[i] = dp[i - 1];
19 bCount++;
20 } else {
21 // If current character is 'a', we have two choices:
22 // 1. Delete this 'a' (cost: dp[i-1] + 1)
23 // 2. Keep this 'a' and delete all 'b's before it (cost: bCount)
24 // Choose the minimum cost option
25 dp[i] = min(dp[i - 1] + 1, bCount);
26 }
27 }
28
29 return dp[stringLength];
30 }
31};
32
1/**
2 * Finds the minimum number of deletions needed to make string balanced.
3 * A string is balanced if there are no 'b' characters before 'a' characters.
4 *
5 * @param s - Input string containing only 'a' and 'b' characters
6 * @returns The minimum number of character deletions required
7 */
8function minimumDeletions(s: string): number {
9 const stringLength: number = s.length;
10
11 // dp[i] represents minimum deletions needed to balance substring s[0...i-1]
12 const dp: number[] = new Array(stringLength + 1).fill(0);
13
14 // Count of 'b' characters encountered so far
15 let bCount: number = 0;
16
17 // Process each character in the string
18 for (let i = 1; i <= stringLength; ++i) {
19 if (s[i - 1] === 'b') {
20 // If current character is 'b', no additional deletion needed
21 // The minimum deletions remain the same as previous position
22 dp[i] = dp[i - 1];
23 ++bCount;
24 } else {
25 // If current character is 'a', we have two choices:
26 // 1. Delete this 'a' (cost: dp[i-1] + 1)
27 // 2. Keep this 'a' and delete all 'b's before it (cost: bCount)
28 dp[i] = Math.min(dp[i - 1] + 1, bCount);
29 }
30 }
31
32 return dp[stringLength];
33}
34
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the string s
. The algorithm iterates through the string once with a single for loop, performing constant-time operations (comparisons, assignments, and min operations) for each character.
The space complexity is O(n)
, not O(1)
as stated in the reference answer. The code creates an array f
of size n + 1
to store the dynamic programming states, which requires linear extra space. The variable b
uses constant space, but the dominant factor is the array f
, making the overall space complexity O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Misunderstanding the Balance Condition
Pitfall: Many people initially think a balanced string means equal numbers of 'a's and 'b's, or that 'a's and 'b's must alternate. This leads to incorrect solutions that try to balance counts rather than positions.
Correct Understanding: A string is balanced when NO 'b' appears before ANY 'a'. Valid balanced strings include:
- "aaabbb" (all 'a's before all 'b's)
- "aaaa" (only 'a's)
- "bbbb" (only 'b's)
- "" (empty string)
2. Incorrect DP State Definition
Pitfall: Defining the DP state as "minimum deletions to balance the entire string up to index i" without properly tracking what characters can still appear after position i.
Solution: The DP state dp[i]
should represent the minimum deletions needed to make the first i characters balanced, considering that any characters can follow after position i.
3. Off-by-One Indexing Errors
Pitfall: Mixing 0-based and 1-based indexing when accessing the string and DP array:
# Incorrect - index mismatch
for i in range(1, n + 1):
if s[i] == 'b': # ERROR: s[i] is out of bounds when i = n
dp[i] = dp[i - 1]
Solution: Use enumerate with proper start value or be consistent with indexing:
# Correct approach 1: enumerate with start=1
for i, char in enumerate(s, 1):
if char == 'b':
dp[i] = dp[i - 1]
# Correct approach 2: 0-based indexing throughout
for i in range(n):
if s[i] == 'b':
dp[i + 1] = dp[i]
4. Not Considering All Options for 'a' Characters
Pitfall: When encountering an 'a', only considering deletion of that 'a' without realizing you could instead delete all previous 'b's:
# Incorrect - missing the option to delete previous b's if char == 'a': dp[i] = dp[i - 1] + 1 # Only considers deleting the 'a'
Solution: Always consider both options:
if char == 'a':
# Option 1: Delete this 'a'
# Option 2: Keep this 'a' and delete all previous 'b's
dp[i] = min(dp[i - 1] + 1, b_count)
5. Forgetting to Update the 'b' Counter
Pitfall: Forgetting to increment b_count
when encountering a 'b' character, leading to incorrect cost calculation for keeping subsequent 'a's:
# Incorrect - forgot to update b_count if char == 'b': dp[i] = dp[i - 1] # Missing: b_count += 1
Solution: Always update the counter when seeing a 'b':
if char == 'b': dp[i] = dp[i - 1] b_count += 1 # Don't forget this!
6. Space Optimization Gone Wrong
Pitfall: Attempting to optimize space to O(1) but incorrectly updating the same variable:
# Incorrect space optimization
def minimumDeletions(self, s: str) -> int:
dp = 0
b_count = 0
for char in s:
if char == 'b':
# dp = dp (redundant)
b_count += 1
else:
dp = min(dp + 1, b_count) # This overwrites dp immediately
return dp
Solution: If optimizing space, use a separate variable for the previous state:
def minimumDeletions(self, s: str) -> int:
min_deletions = 0
b_count = 0
for char in s:
if char == 'b':
b_count += 1
else:
min_deletions = min(min_deletions + 1, b_count)
return min_deletions
In a binary min heap, the maximum element can be found in:
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
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
Want a Structured Path to Master System Design Too? Don’t Miss This!