Facebook Pixel

3746. Minimum String Length After Balanced Removals

MediumStackStringCounting
LeetCode ↗

Problem Description

You are given a string s consisting only of the characters 'a' and 'b'.

You are allowed to repeatedly remove any substring where the number of 'a' characters is equal to the number of 'b' characters. After each removal, the remaining parts of the string are concatenated together without gaps.

Return an integer denoting the minimum possible length of the string after performing any number of such operations.

Example:

Suppose s = "aabbab". We can remove substrings whose count of 'a' equals the count of 'b'. For instance, we may remove "aabb" (2 a's and 2 b's), leaving "ab", and then remove "ab" (1 a and 1 b), leaving an empty string. The minimum possible length here is 0.

The key observation is that whenever two adjacent characters differ (one 'a' and one 'b'), they form a valid substring of equal counts and can be removed. By repeatedly removing such pairs, the string keeps shrinking until only one type of character remains. The leftover characters are simply the surplus of whichever character appears more frequently.

Therefore, if a is the count of 'a' and b is the count of 'b', the minimum possible length is the absolute difference between them, i.e. abs(a - b).

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

How We Pick the Algorithm

Why Stack?

This problem maps to Stack through a short path in the full flowchart.

Parsesymbols?yesOptimizevalidspans?noStack

The LIFO matching behavior of brackets or elements maps to stack operations.

Open in Flowchart

Intuition

The core idea is to figure out which characters can actually be eliminated and which ones must remain no matter what we do.

Notice that whenever we have two adjacent characters that are different — that is, an 'a' next to a 'b' (or a 'b' next to an 'a') — they form a substring with exactly one 'a' and one 'b'. Since their counts are equal, this substring can be removed. After removing it, the characters that were on either side become adjacent, possibly creating new removable pairs.

This means we can keep "canceling out" pairs of different characters, much like matching opposite signs. Think of it as pushing characters together: every time an 'a' meets a 'b', they annihilate each other. So the question becomes — after canceling as many 'a'-'b' pairs as possible, what is left?

If we have a copies of 'a' and b copies of 'b', each removal consumes exactly one of each. We can keep removing until one type runs out. The character that appears more often will have some leftover copies that have nothing to pair with, and these can never be removed because any remaining substring would consist of only one type of character (which can never have equal counts unless it's empty).

So the final string consists purely of the surplus characters of whichever letter is more frequent. The number of these leftover characters is exactly abs(a - b), which is our minimum possible length. This also explains why the actual order of characters in s does not matter — only the counts of 'a' and 'b' determine the answer.

Pattern Learn more about Stack patterns.

Solution Approach

Solution 1: Counting

Based on the intuition above, the order of characters doesn't matter — only how many 'a' and 'b' characters exist. Every 'a'-'b' pair can eventually be canceled out, so all that remains is the surplus of the more frequent character. This reduces the entire problem to a simple counting task.

The implementation follows these steps:

  1. Count the 'a' characters. Use s.count("a") to get the total number of 'a' characters and store it in a.

  2. Derive the 'b' count. Since the string contains only 'a' and 'b', the number of 'b' characters is simply the total length minus the count of 'a', i.e. b = len(s) - a. This avoids scanning the string a second time.

  3. Return the surplus. The minimum possible length equals the absolute difference between the two counts, abs(a - b), which represents the leftover characters that can never be paired and removed.

No extra data structures or complex patterns are needed — just two counts and one subtraction.

The implementation is shown below:

class Solution:
    def minLengthAfterRemovals(self, s: str) -> int:
        a = s.count("a")
        b = len(s) - a
        return abs(a - b)

Complexity Analysis:

  • Time Complexity: O(n), where n is the length of the string s. Counting the 'a' characters requires a single pass over the string.
  • Space Complexity: O(1), since only a constant number of integer variables are used regardless of the input size.

Example Walkthrough

Let's trace through the solution approach with a small example: s = "abba".

Step 1: Understand what we're working with

The string s = "abba" has the following characters:

Index:  0   1   2   3
Char:   a   b   b   a

Step 2: Count the 'a' characters

We call s.count("a"). Scanning the string, we find 'a' at index 0 and index 3:

a = 2

Step 3: Derive the 'b' count

Rather than scanning again, we compute the number of 'b' characters using the total length:

b = len(s) - a = 4 - 2 = 2

Step 4: Return the surplus

The minimum possible length is the absolute difference between the two counts:

abs(a - b) = abs(2 - 2) = 0

So the answer is 0.

Why this matches the intuition (verifying by hand)

Let's confirm by actually performing removals on "abba":

  • The middle two characters "bb"? No — that has 2 b's and 0 a's, not removable.
  • Look at indices 1 and 2... but a better target is the substring "abba" itself: it contains 2 a's and 2 b's, so it's a valid removal. Removing it leaves the empty string "".

Alternatively, removing pairs gradually:

  • Remove "ab" (indices 0–1): "abba""ba"
  • Remove "ba" (a valid pair, 1 'a' and 1 'b'): "ba"""

Either path reaches an empty string of length 0, which exactly equals our computed abs(a - b) = 0. Since the counts of 'a' and 'b' are equal, every character finds a partner to cancel with, leaving nothing behind — confirming that only the counts (not the order) determine the result.

Solution Implementation

1class Solution:
2    def minLengthAfterRemovals(self, s: str) -> int:
3        # Count the number of 'a' characters in the string
4        count_a = s.count("a")
5
6        # The remaining characters are treated as 'b'
7        count_b = len(s) - count_a
8
9        # The minimum remaining length is the absolute difference
10        # between the two counts, since each pair of differing
11        # characters can be removed together
12        return abs(count_a - count_b)
13```
14
15**A note on perspective:**
16
17The original variable names (`a`, `b`) suggested the string consists only of two character types. If the real problem involves a stack-based removal of specific adjacent patterns (e.g., removing `"ab"` or `"ba"` substrings), a more general solution would use a stack:
18
19```python3
20class Solution:
21    def minLengthAfterRemovals(self, s: str) -> int:
22        # Use a stack to simulate removing adjacent matching pairs
23        stack: list[str] = []
24        for char in s:
25            # If the current char forms a removable pair with the top,
26            # pop it instead of pushing
27            if stack and stack[-1] != char:
28                stack.pop()
29            else:
30                stack.append(char)
31        # Remaining elements could not be paired off
32        return len(stack)
33
1class Solution {
2    /**
3     * Counts how many characters remain after repeatedly removing pairs.
4     * The string is conceptually split into two groups: 'a' characters and
5     * non-'a' characters. Each removal pairs one character from each group,
6     * so the number left over equals the absolute difference of the two counts.
7     *
8     * @param s the input string to process
9     * @return the minimum length remaining after all possible removals
10     */
11    public int minLengthAfterRemovals(String s) {
12        int length = s.length();
13
14        // Count the number of 'a' characters in the string.
15        int countA = 0;
16        for (int i = 0; i < length; i++) {
17            if (s.charAt(i) == 'a') {
18                countA++;
19            }
20        }
21
22        // The remaining characters belong to the non-'a' group.
23        int countOthers = length - countA;
24
25        // After pairing characters from both groups, the leftover count
26        // is the absolute difference between the two group sizes.
27        return Math.abs(countA - countOthers);
28    }
29}
30
1class Solution {
2public:
3    int minLengthAfterRemovals(string s) {
4        // Count the number of 'a' characters in the string
5        int countA = 0;
6        for (char ch : s) {
7            if (ch == 'a') {
8                ++countA;
9            }
10        }
11
12        // The remaining characters are all non-'a' characters
13        int countOther = static_cast<int>(s.size()) - countA;
14
15        // Return the absolute difference between the two counts
16        return abs(countA - countOther);
17    }
18};
19
1/**
2 * Calculates the minimum length of a string after performing removals.
3 *
4 * The string characters are partitioned into two groups:
5 *   - characters equal to 'a'
6 *   - all other characters
7 * Pairs from opposing groups cancel each other out, so the final
8 * remaining length is the absolute difference between the two counts.
9 *
10 * @param s - The input string to process.
11 * @returns The minimum length remaining after removals.
12 */
13function minLengthAfterRemovals(s: string): number {
14    // Count of characters equal to 'a'.
15    let countA = 0;
16
17    // Iterate through each character and tally the 'a' occurrences.
18    for (const char of s) {
19        if (char === 'a') {
20            ++countA;
21        }
22    }
23
24    // Count of all characters that are not 'a'.
25    const countOther = s.length - countA;
26
27    // The unmatched remainder equals the absolute difference of the two groups.
28    return Math.abs(countA - countOther);
29}
30

Time and Space Complexity

  • Time Complexity: O(n), where n is the length of the string s. The operation s.count("a") traverses the entire string once to count the occurrences of the character "a", taking O(n) time. Computing len(s) is O(1), and the remaining arithmetic operations (subtraction and abs) are also O(1). Therefore, the overall time complexity is dominated by the counting operation, resulting in O(n).

  • Space Complexity: O(1). The algorithm only uses a constant amount of extra space to store the variables a and b, regardless of the input size. No additional data structures that scale with the input are used.

Pattern Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Assuming Adjacency Constraints Limit Removals

The most common mistake is over-engineering the solution with a stack, believing that only adjacent differing pairs (like "ab" or "ba") can be removed. The problem actually allows removing any substring where the count of 'a' equals the count of 'b'—not just adjacent opposite characters.

This distinction matters: the stack approach assumes characters can only cancel when they meet at the top of the stack, but in reality, the order is irrelevant. Consider s = "aabb":

  • Stack approach result: push 'a', push 'a', then 'b' cancels one 'a', then 'b' cancels another 'a' → length 0. ✓ (happens to work here)
  • But for s = "abba": push 'a', push 'b' cancels 'a', push 'b', push 'a' cancels 'b' → length 0. ✓

While the stack often produces the same numeric answer because every 'a' eventually finds a 'b', treating this as a constrained adjacency problem leads to confusion and unnecessary complexity. The correct mental model is pure counting.

Solution: Recognize that since the whole string is a valid removal target whenever counts are balanced, only the surplus survives. Stick with abs(count_a - count_b).

Pitfall 2: Scanning the String Twice

A subtle inefficiency is counting both characters separately:

# Less efficient — two passes
count_a = s.count("a")
count_b = s.count("b")
return abs(count_a - count_b)

Since the string contains only 'a' and 'b', the second scan is redundant.

Solution: Derive the 'b' count arithmetically from the total length:

count_a = s.count("a")
count_b = len(s) - count_a   # single pass
return abs(count_a - count_b)

Pitfall 3: Forgetting the Absolute Value

Returning count_a - count_b directly can yield a negative result when 'b' characters outnumber 'a' characters (e.g., s = "abbb" gives 1 - 3 = -2). A length can never be negative.

Solution: Always wrap the difference in abs():

return abs(count_a - count_b)

Pitfall 4: Mishandling Edge Cases

Failing to account for boundary inputs can cause errors:

  • Empty string (s = ""): both counts are 0, correctly returning 0.
  • Single character (s = "a"): returns abs(1 - 0) = 1, which is correct since a lone character can never be paired.
  • All identical characters (s = "aaaa"): returns abs(4 - 0) = 4, as no removals are possible.

Solution: The counting formula naturally handles all these cases without special branching—verify them mentally rather than adding redundant guard clauses.

Ready to land your dream job?

Unlock your dream job with a 5-minute quiz for a personalized study roadmap!

Get My Roadmap
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Get a Personalized Study Roadmap:

What's the output of running the following function using the following tree as input?

1def serialize(root):
2    res = []
3    def dfs(root):
4        if not root:
5            res.append('x')
6            return
7        res.append(root.val)
8        dfs(root.left)
9        dfs(root.right)
10    dfs(root)
11    return ' '.join(res)
12
1import java.util.StringJoiner;
2
3public static String serialize(Node root) {
4    StringJoiner res = new StringJoiner(" ");
5    serializeDFS(root, res);
6    return res.toString();
7}
8
9private static void serializeDFS(Node root, StringJoiner result) {
10    if (root == null) {
11        result.add("x");
12        return;
13    }
14    result.add(Integer.toString(root.val));
15    serializeDFS(root.left, result);
16    serializeDFS(root.right, result);
17}
18
1function serialize(root) {
2    let res = [];
3    serialize_dfs(root, res);
4    return res.join(" ");
5}
6
7function serialize_dfs(root, res) {
8    if (!root) {
9        res.push("x");
10        return;
11    }
12    res.push(root.val);
13    serialize_dfs(root.left, res);
14    serialize_dfs(root.right, res);
15}
16

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More