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:
-
Remove substring
"ab"
and gainx
points.- For example, when removing
"ab"
from"cabxbae"
, it becomes"cxbae"
.
- For example, when removing
-
Remove substring
"ba"
and gainy
points.- For example, when removing
"ba"
from"cabxbae"
, it becomes"cabxe"
.
- For example, when removing
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 earnx
points - Then remove the remaining
"ab"
to earn anotherx
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.
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 calculatemin(cnt_a, cnt_b) * lower_value
This approach ensures we always prioritize the higher-value operations while processing the string in linear time.
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 scorecnt1
: 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 futureb
. -
Case 2:
c == b
(second character of high-value pattern)if cnt1: ans += x cnt1 -= 1 else: cnt2 += 1
If we have unpaired
a
s (cnt1 > 0
), we can form the high-value pattern immediately, earningx
points. Otherwise, we store thisb
incnt2
for potential low-value pairing later. -
Case 3:
c
is neithera
norb
ans += min(cnt1, cnt2) * y cnt1 = cnt2 = 0
This character acts as a segment boundary. Any remaining unpaired
a
s andb
s can only form low-value patterns. We addmin(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 EvaluatorExample 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:
-
'a' (index 0): This is our
b
character (after swap)- No
cnt1
, socnt2 = 1
- State:
cnt1 = 0
,cnt2 = 1
,ans = 0
- No
-
'a' (index 1): Another
b
character- No
cnt1
, socnt2 = 2
- State:
cnt1 = 0
,cnt2 = 2
,ans = 0
- No
-
'b' (index 2): This is our
a
character (after swap)cnt1 = 1
- State:
cnt1 = 1
,cnt2 = 2
,ans = 0
-
'b' (index 3): Another
a
charactercnt1 = 2
- State:
cnt1 = 2
,cnt2 = 2
,ans = 0
-
'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
- We have
-
'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
- Form low-value pairs:
-
'y' (index 6): Neither 'a' nor 'b' - segment boundary!
min(0, 0) * 4 = 0
(nothing to pair)- State:
cnt1 = 0
,cnt2 = 0
,ans = 9
-
'a' (index 7): This is our
b
character- No
cnt1
, socnt2 = 1
- State:
cnt1 = 0
,cnt2 = 1
,ans = 9
- No
-
'b' (index 8): This is our
a
charactercnt1 = 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
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
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
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
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!