Facebook Pixel

1717. Maximum Score From Removing Substrings

Problem Description

You are given a string s and two integers x and y. You can perform two types of operations any number of times:

  1. Remove substring "ab" and gain x points.

    • For example, when removing "ab" from "cabxbae", it becomes "cxbae".
  2. Remove substring "ba" and gain y points.

    • For example, when removing "ba" from "cabxbae", it becomes "cabxe".

Your goal is to return the maximum points you can gain after applying these operations on string s.

The key insight is that you can perform these operations multiple times in any order. Each time you remove either "ab" or "ba", you earn the corresponding points (x or y). The challenge is to determine the optimal sequence of removals to maximize your total score.

For example, if you have the string "aabb", you could:

  • Remove "ab" first to get "ab" and earn x points
  • Then remove the remaining "ab" to earn another x points
  • Total: 2x points

Or you could manipulate the order differently if that yields more points based on the values of x and y.

The problem requires finding the strategy that maximizes the total points earned from all possible removal operations.

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

Intuition

The first key observation is that we should prioritize removing the substring that gives us more points. If x > y, we should try to remove "ab" as much as possible before removing "ba". If y > x, we should do the opposite.

Why does this greedy approach work? Consider what happens when we have a mix of 'a's and 'b's. Each removal operation consumes exactly one 'a' and one 'b'. No matter what order we perform the operations, the total number of operations we can perform is fixed - it's min(count_of_a, count_of_b).

Since the total number of operations is fixed, we want to maximize the value we get from these operations. The greedy strategy is to perform the higher-value operations first, and only perform the lower-value operations with whatever characters are left.

Another crucial insight is that characters other than 'a' and 'b' act as barriers. They split the string into independent segments. For example, in "aabcbba", the 'c' divides it into "aab" and "bba", which we can process separately.

The clever part of the solution is how it processes the string in a single pass. As we traverse:

  • When we see an 'a', we hold onto it (increment counter) hoping to pair it with a future 'b' for the higher-value operation
  • When we see a 'b', we check if we have any 'a's waiting. If yes, we immediately form the high-value pair. If no, we hold onto this 'b'
  • When we see any other character, we know we've reached the end of a segment. Any unpaired 'a's and 'b's in this segment can only form the lower-value pairs, so we calculate min(cnt_a, cnt_b) * lower_value

This approach ensures we always prioritize the higher-value operations while processing the string in linear time.

Learn more about Stack and Greedy patterns.

Solution Approach

The implementation follows a greedy strategy with a single-pass traversal:

Step 1: Normalize the problem

a, b = "a", "b"
if x < y:
    x, y = y, x
    a, b = b, a

We ensure that x ≥ y by swapping if necessary. This means we always prioritize removing the pattern stored in variables a and b (which forms "ab" or "ba" depending on the swap).

Step 2: Initialize counters

ans = cnt1 = cnt2 = 0
  • ans: accumulates the total score
  • cnt1: counts unpaired instances of the first character (a)
  • cnt2: counts unpaired instances of the second character (b)

Step 3: Process each character For each character c in the string:

  • Case 1: c == a (first character of high-value pattern)

    cnt1 += 1

    We increment cnt1 to track this character, hoping to pair it with a future b.

  • Case 2: c == b (second character of high-value pattern)

    if cnt1:
        ans += x
        cnt1 -= 1
    else:
        cnt2 += 1

    If we have unpaired as (cnt1 > 0), we can form the high-value pattern immediately, earning x points. Otherwise, we store this b in cnt2 for potential low-value pairing later.

  • Case 3: c is neither a nor b

    ans += min(cnt1, cnt2) * y
    cnt1 = cnt2 = 0

    This character acts as a segment boundary. Any remaining unpaired as and bs can only form low-value patterns. We add min(cnt1, cnt2) * y to the score and reset both counters.

Step 4: Handle remaining characters

ans += min(cnt1, cnt2) * y

After processing all characters, we handle any remaining unpaired characters at the end of the string, treating them as low-value patterns.

This algorithm runs in O(n) time with O(1) extra space, where n is the length of the string. The key insight is that by always greedily forming high-value patterns first during traversal, we ensure maximum score without needing to actually modify the string or use additional data structures like stacks.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the solution with string s = "aabbaxyab", x = 4, and y = 5.

Since y > x, we first swap values: x = 5, y = 4, and swap patterns: a = "b", b = "a". Now we prioritize removing "ba" (worth 5 points) over "ab" (worth 4 points).

Initial state: ans = 0, cnt1 = 0, cnt2 = 0

Processing each character:

  1. 'a' (index 0): This is our b character (after swap)

    • No cnt1, so cnt2 = 1
    • State: cnt1 = 0, cnt2 = 1, ans = 0
  2. 'a' (index 1): Another b character

    • No cnt1, so cnt2 = 2
    • State: cnt1 = 0, cnt2 = 2, ans = 0
  3. 'b' (index 2): This is our a character (after swap)

    • cnt1 = 1
    • State: cnt1 = 1, cnt2 = 2, ans = 0
  4. 'b' (index 3): Another a character

    • cnt1 = 2
    • State: cnt1 = 2, cnt2 = 2, ans = 0
  5. 'a' (index 4): This is our b character

    • We have cnt1 > 0, so we can form "ba"!
    • ans += 5, cnt1 -= 1
    • State: cnt1 = 1, cnt2 = 2, ans = 5
  6. 'x' (index 5): Neither 'a' nor 'b' - segment boundary!

    • Form low-value pairs: min(1, 2) * 4 = 4
    • ans += 4, reset counters
    • State: cnt1 = 0, cnt2 = 0, ans = 9
  7. 'y' (index 6): Neither 'a' nor 'b' - segment boundary!

    • min(0, 0) * 4 = 0 (nothing to pair)
    • State: cnt1 = 0, cnt2 = 0, ans = 9
  8. 'a' (index 7): This is our b character

    • No cnt1, so cnt2 = 1
    • State: cnt1 = 0, cnt2 = 1, ans = 9
  9. 'b' (index 8): This is our a character

    • cnt1 = 1
    • State: cnt1 = 1, cnt2 = 1, ans = 9

Final step: Handle remaining characters

  • min(1, 1) * 4 = 4
  • ans = 9 + 4 = 13

Result: Maximum points = 13

The algorithm successfully identified that we could form one "ba" (5 points) in the first segment, one "ab" (4 points) from the remaining characters in that segment, and one "ab" (4 points) in the last segment, giving us a total of 13 points.

Solution Implementation

1class Solution:
2    def maximumGain(self, s: str, x: int, y: int) -> int:
3        # Ensure we always process the higher-value pattern first
4        # If 'ba' is worth more than 'ab', swap everything
5        first_char, second_char = "a", "b"
6        if x < y:
7            x, y = y, x
8            first_char, second_char = second_char, first_char
9      
10        # Initialize counters and total score
11        total_score = 0
12        first_char_count = 0  # Count of unmatched first characters
13        second_char_count = 0  # Count of unmatched second characters
14      
15        # Process each character in the string
16        for current_char in s:
17            if current_char == first_char:
18                # Found the first character of potential pattern
19                first_char_count += 1
20            elif current_char == second_char:
21                # Found the second character
22                if first_char_count > 0:
23                    # Can form the higher-value pattern (first_char + second_char)
24                    total_score += x
25                    first_char_count -= 1
26                else:
27                    # No first_char available, store for potential lower-value pattern
28                    second_char_count += 1
29            else:
30                # Non-a/b character encountered - process remaining characters
31                # Form as many lower-value patterns as possible from leftovers
32                total_score += min(first_char_count, second_char_count) * y
33                # Reset counters for next segment
34                first_char_count = 0
35                second_char_count = 0
36      
37        # Process any remaining characters at the end of string
38        total_score += min(first_char_count, second_char_count) * y
39      
40        return total_score
41
1class Solution {
2    public int maximumGain(String s, int x, int y) {
3        // Characters for pattern matching
4        char firstChar = 'a';
5        char secondChar = 'b';
6      
7        // Ensure we process the pattern with higher points first
8        // If x < y, swap the values and characters to prioritize "ba" over "ab"
9        if (x < y) {
10            // Swap point values
11            int tempPoints = x;
12            x = y;
13            y = tempPoints;
14          
15            // Swap character order
16            char tempChar = firstChar;
17            firstChar = secondChar;
18            secondChar = tempChar;
19        }
20      
21        // Initialize result and counters
22        int totalPoints = 0;
23        int firstCharCount = 0;  // Count of unmatched first characters
24        int secondCharCount = 0; // Count of unmatched second characters
25      
26        int stringLength = s.length();
27      
28        // Process each character in the string
29        for (int i = 0; i < stringLength; ++i) {
30            char currentChar = s.charAt(i);
31          
32            if (currentChar == firstChar) {
33                // Accumulate first character
34                firstCharCount++;
35            } else if (currentChar == secondChar) {
36                if (firstCharCount > 0) {
37                    // Can form the higher-value pattern (firstChar + secondChar)
38                    totalPoints += x;
39                    firstCharCount--;
40                } else {
41                    // No first character available, accumulate second character
42                    secondCharCount++;
43                }
44            } else {
45                // Non-target character encountered
46                // Process remaining characters with lower-value pattern
47                totalPoints += Math.min(firstCharCount, secondCharCount) * y;
48              
49                // Reset counters for next segment
50                firstCharCount = 0;
51                secondCharCount = 0;
52            }
53        }
54      
55        // Process any remaining characters at the end of string
56        totalPoints += Math.min(firstCharCount, secondCharCount) * y;
57      
58        return totalPoints;
59    }
60}
61
1class Solution {
2public:
3    int maximumGain(string s, int x, int y) {
4        // Ensure we always process the higher-valued pattern first
5        // If x < y, we swap values and characters to make 'a' + 'b' the higher-valued pattern
6        char firstChar = 'a';
7        char secondChar = 'b';
8        if (x < y) {
9            swap(x, y);
10            swap(firstChar, secondChar);
11        }
12      
13        // Now firstChar + secondChar gives x points (higher value)
14        // secondChar + firstChar gives y points (lower value)
15      
16        int totalScore = 0;
17        int firstCharCount = 0;   // Count of unmatched firstChar characters
18        int secondCharCount = 0;  // Count of unmatched secondChar characters
19      
20        // Process each character in the string
21        for (char currentChar : s) {
22            if (currentChar == firstChar) {
23                // Accumulate firstChar for potential future matching
24                firstCharCount++;
25            } 
26            else if (currentChar == secondChar) {
27                if (firstCharCount > 0) {
28                    // We can form the high-value pattern (firstChar + secondChar)
29                    totalScore += x;
30                    firstCharCount--;  // Consume one firstChar
31                } else {
32                    // No firstChar available, accumulate secondChar for lower-value pattern
33                    secondCharCount++;
34                }
35            } 
36            else {
37                // Non-'a' and non-'b' character encountered
38                // Process any remaining characters to form lower-value patterns
39                totalScore += min(firstCharCount, secondCharCount) * y;
40              
41                // Reset counters as we can't carry over across non-ab characters
42                firstCharCount = 0;
43                secondCharCount = 0;
44            }
45        }
46      
47        // Process any remaining characters at the end of the string
48        totalScore += min(firstCharCount, secondCharCount) * y;
49      
50        return totalScore;
51    }
52};
53
1/**
2 * Calculates the maximum gain by removing substrings "ab" and "ba" from the input string.
3 * Each removal of "ab" gives x points and each removal of "ba" gives y points.
4 * 
5 * @param s - The input string to process
6 * @param x - Points gained for removing "ab" substring
7 * @param y - Points gained for removing "ba" substring
8 * @returns The maximum total points that can be obtained
9 */
10function maximumGain(s: string, x: number, y: number): number {
11    // Initialize characters to look for in order of priority
12    let firstChar: string = 'a';
13    let secondChar: string = 'b';
14  
15    // If "ba" gives more points than "ab", swap the priorities
16    if (x < y) {
17        [x, y] = [y, x];
18        [firstChar, secondChar] = [secondChar, firstChar];
19    }
20  
21    // Initialize result and counters for unmatched characters
22    let totalPoints: number = 0;
23    let firstCharCount: number = 0;
24    let secondCharCount: number = 0;
25  
26    // Process each character in the string
27    for (const currentChar of s) {
28        if (currentChar === firstChar) {
29            // Count occurrences of the first priority character
30            firstCharCount++;
31        } else if (currentChar === secondChar) {
32            if (firstCharCount > 0) {
33                // Form a high-priority pair and add points
34                totalPoints += x;
35                firstCharCount--;
36            } else {
37                // No first character available, count this second character
38                secondCharCount++;
39            }
40        } else {
41            // Non-target character encountered, process remaining characters
42            // Form as many low-priority pairs as possible
43            totalPoints += Math.min(firstCharCount, secondCharCount) * y;
44            firstCharCount = 0;
45            secondCharCount = 0;
46        }
47    }
48  
49    // Process any remaining characters at the end of the string
50    totalPoints += Math.min(firstCharCount, secondCharCount) * y;
51  
52    return totalPoints;
53}
54

Time and Space Complexity

The time complexity is O(n), where n is the length of string s. The algorithm iterates through the string exactly once using a single for loop that processes each character. Each operation inside the loop (comparisons, arithmetic operations, and variable assignments) takes constant time O(1). Therefore, the overall time complexity is linear with respect to the input size.

The space complexity is O(1). The algorithm uses only a fixed number of variables (a, b, ans, cnt1, cnt2, x, y) regardless of the input size. No additional data structures that grow with the input are created. The space usage remains constant throughout the execution of the algorithm.

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

Common Pitfalls

1. Not Handling the Greedy Order Correctly

Pitfall: A common mistake is trying to remove both "ab" and "ba" patterns simultaneously without considering which one should be prioritized. Simply scanning for both patterns in a single pass will lead to suboptimal results.

Why it happens: Consider the string "aabb" with x=4 and y=1. If you naively remove patterns as you encounter them without prioritization, you might remove "ba" first (middle characters), leaving you with "ab" for a total of 5 points. However, removing two "ab" patterns would give you 8 points.

Solution: Always prioritize removing the higher-value pattern first. The code handles this by normalizing the problem - ensuring x ≥ y through swapping, then greedily forming the higher-value pattern during traversal.

2. Using String Manipulation Instead of Counters

Pitfall: Actually removing substrings from the string during processing, like using s = s.replace("ab", "") repeatedly.

Why it's problematic:

  • String manipulation in Python creates new strings (O(n) per operation)
  • Multiple removals can lead to O(n²) time complexity
  • After each removal, you need to rescan the string for new patterns that may have formed

Example problematic code:

# DON'T DO THIS
while "ab" in s or "ba" in s:
    if x >= y and "ab" in s:
        s = s.replace("ab", "", 1)
        score += x
    elif "ba" in s:
        s = s.replace("ba", "", 1)
        score += y

Solution: Use counters to track unpaired characters instead of modifying the string. This maintains O(n) time complexity with a single pass.

3. Forgetting to Process Remaining Characters

Pitfall: Only processing characters when encountering non-a/b characters as segment boundaries, but forgetting to handle leftover characters at the end of the string.

Example: For string "abab", if you only process at segment boundaries (non-a/b characters), you'd miss the final characters and lose potential points.

Solution: Always add a final processing step after the loop:

# Don't forget this line!
total_score += min(first_char_count, second_char_count) * y

4. Incorrect Swap Logic

Pitfall: When x < y, only swapping the values but not the character mappings, or vice versa.

Incorrect approach:

# Wrong - only swapping values
if x < y:
    x, y = y, x
    # But still looking for "ab" first instead of "ba"

Solution: Swap both the point values AND the character order to maintain consistency:

if x < y:
    x, y = y, x
    first_char, second_char = "b", "a"  # Also swap the pattern
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

What's the output of running the following function using input [30, 20, 10, 100, 33, 12]?

1def fun(arr: List[int]) -> List[int]:
2    import heapq
3    heapq.heapify(arr)
4    res = []
5    for i in range(3):
6        res.append(heapq.heappop(arr))
7    return res
8
1public static int[] fun(int[] arr) {
2    int[] res = new int[3];
3    PriorityQueue<Integer> heap = new PriorityQueue<>();
4    for (int i = 0; i < arr.length; i++) {
5        heap.add(arr[i]);
6    }
7    for (int i = 0; i < 3; i++) {
8        res[i] = heap.poll();
9    }
10    return res;
11}
12
1class HeapItem {
2    constructor(item, priority = item) {
3        this.item = item;
4        this.priority = priority;
5    }
6}
7
8class MinHeap {
9    constructor() {
10        this.heap = [];
11    }
12
13    push(node) {
14        // insert the new node at the end of the heap array
15        this.heap.push(node);
16        // find the correct position for the new node
17        this.bubble_up();
18    }
19
20    bubble_up() {
21        let index = this.heap.length - 1;
22
23        while (index > 0) {
24            const element = this.heap[index];
25            const parentIndex = Math.floor((index - 1) / 2);
26            const parent = this.heap[parentIndex];
27
28            if (parent.priority <= element.priority) break;
29            // if the parent is bigger than the child then swap the parent and child
30            this.heap[index] = parent;
31            this.heap[parentIndex] = element;
32            index = parentIndex;
33        }
34    }
35
36    pop() {
37        const min = this.heap[0];
38        this.heap[0] = this.heap[this.size() - 1];
39        this.heap.pop();
40        this.bubble_down();
41        return min;
42    }
43
44    bubble_down() {
45        let index = 0;
46        let min = index;
47        const n = this.heap.length;
48
49        while (index < n) {
50            const left = 2 * index + 1;
51            const right = left + 1;
52
53            if (left < n && this.heap[left].priority < this.heap[min].priority) {
54                min = left;
55            }
56            if (right < n && this.heap[right].priority < this.heap[min].priority) {
57                min = right;
58            }
59            if (min === index) break;
60            [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61            index = min;
62        }
63    }
64
65    peek() {
66        return this.heap[0];
67    }
68
69    size() {
70        return this.heap.length;
71    }
72}
73
74function fun(arr) {
75    const heap = new MinHeap();
76    for (const x of arr) {
77        heap.push(new HeapItem(x));
78    }
79    const res = [];
80    for (let i = 0; i < 3; i++) {
81        res.push(heap.pop().item);
82    }
83    return res;
84}
85

Recommended Readings

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

Load More