1898. Maximum Number of Removable Characters
Problem Description
You have two strings s
and p
, where p
is a subsequence of s
. You also have a distinct array removable
containing indices of characters in s
(using 0-based indexing).
Your task is to find the maximum number k
such that after removing the first k
characters indicated by the indices in removable
from string s
, the string p
remains a subsequence of the modified s
.
Here's how the removal process works:
- You choose a value
k
where0 <= k <= removable.length
- You mark the characters at positions
s[removable[0]]
,s[removable[1]]
, ...,s[removable[k-1]]
- You remove all marked characters from
s
- You check if
p
is still a subsequence of the remaining string
A subsequence means that p
can be formed from s
by deleting some (or no) characters without changing the order of the remaining characters.
For example, if s = "abcab"
, p = "ab"
, and removable = [0, 1, 2]
:
- If
k = 0
: no removals,p
is still a subsequence ofs
- If
k = 1
: removes[0]
('a'), resulting in "bcab",p
is still a subsequence - If
k = 2
: removes[0]
ands[1]
('a' and 'b'), resulting in "cab",p
is still a subsequence - If
k = 3
: removes[0]
,s[1]
, ands[2]
('a', 'b', and 'c'), resulting in "ab",p
is still a subsequence
The goal is to find the largest possible k
value that maintains p
as a subsequence of the modified s
.
Intuition
The key observation is that this problem has a monotonic property: if removing the first k
characters from removable
still keeps p
as a subsequence of s
, then removing any fewer characters (like k-1
, k-2
, etc.) will definitely keep p
as a subsequence as well. Conversely, if removing k
characters breaks the subsequence relationship, then removing more characters (k+1
, k+2
, etc.) will also break it.
Think of it this way: as we remove more characters from s
, we're making it harder and harder for p
to remain a subsequence. There's a tipping point where we've removed too many characters and p
can no longer be formed from the remaining string.
This monotonic behavior is perfect for binary search. We're essentially looking for the boundary - the maximum value of k
where the condition still holds true.
We can visualize this as a sequence of true/false values:
k=0
: TRUE (p is subsequence)k=1
: TRUE (p is subsequence)- ...
k=m
: TRUE (p is subsequence)k=m+1
: FALSE (p is NOT subsequence)k=m+2
: FALSE (p is NOT subsequence)- ...
We want to find that last TRUE value, which is m
in this case.
For each candidate value of k
during our binary search, we need to check if p
is still a subsequence after removal. We do this by marking the first k
indices from removable
as "removed" and then using a two-pointer technique to verify if we can still match all characters of p
in order within the non-removed characters of s
.
The binary search range is from 0
(remove nothing) to len(removable)
(remove all possible characters), and we're looking for the rightmost position where the condition is satisfied.
Learn more about Two Pointers and Binary Search patterns.
Solution Approach
The solution uses binary search combined with a subsequence checking function.
Binary Search Setup: We set up our search boundaries:
- Left boundary
l = 0
(minimum removals) - Right boundary
r = len(removable)
(maximum possible removals)
The Check Function:
For a given value k
, we need to verify if p
remains a subsequence of s
after removing the first k
characters indicated by removable
:
- Create a boolean array
rem
of sizelen(s)
to mark which characters are removed - Mark the first
k
indices fromremovable
asTrue
in therem
array - Use two pointers to check if
p
is a subsequence:- Pointer
i
traverses through strings
- Pointer
j
traverses through stringp
- If
s[i]
is not removed (rem[i]
isFalse
) and matchesp[j]
, incrementj
- Always increment
i
to move throughs
- Pointer
- Return
True
if we've matched all characters ofp
(i.e.,j == len(p)
)
Binary Search Process:
While l < r
:
- Calculate the middle point:
mid = (l + r + 1) >> 1
- The
>> 1
is a bitwise right shift, equivalent to integer division by 2 - We use
(l + r + 1)
instead of(l + r)
to bias toward the upper half when the range has even length
- The
- Check if removing the first
mid
characters still maintains the subsequence:- If
check(mid)
returnsTrue
: We can potentially remove more, so updatel = mid
- If
check(mid)
returnsFalse
: We've removed too many, so updater = mid - 1
- If
Why the +1
in (l + r + 1) >> 1
?
This prevents infinite loops when l
and r
are adjacent. Without it, if l = 2
and r = 3
, then mid = (2 + 3) // 2 = 2
, and if the check passes, we'd set l = 2
again, causing an infinite loop. With +1
, we get mid = 3
, ensuring progress.
The algorithm terminates when l == r
, and this value represents the maximum number of removals possible while keeping p
as a subsequence of s
.
Time Complexity: O(n * log m)
where n
is the length of string s
and m
is the length of the removable
array. We perform O(log m)
binary search iterations, and each check operation takes O(n)
time.
Space Complexity: O(n)
for the boolean array used in the check function.
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 a concrete example to understand how the solution works.
Given:
s = "abcbddddd"
p = "abcd"
removable = [3, 2, 1, 4, 5, 6]
Goal: Find the maximum k
such that after removing characters at indices removable[0]
through removable[k-1]
from s
, the string p
remains a subsequence.
Binary Search Process:
Initial setup: l = 0
, r = 6
(length of removable array)
Iteration 1:
mid = (0 + 6 + 1) >> 1 = 3
- Check if we can remove first 3 elements:
removable[0:3] = [3, 2, 1]
- Mark positions 3, 2, 1 as removed in string
s
:- Original:
a b c b d d d d d
(indices 0-8) - Removed:
a X X X d d d d d
(X marks removed) - Actual:
a d d d d d
- Original:
- Check if
p = "abcd"
is subsequence of"addddd"
:- Match 'a' at position 0 ✓
- Can't find 'b' ✗
- Result: FALSE, so
r = mid - 1 = 2
Iteration 2:
l = 0
,r = 2
mid = (0 + 2 + 1) >> 1 = 1
- Check if we can remove first 1 element:
removable[0:1] = [3]
- Mark position 3 as removed:
- Original:
a b c b d d d d d
- Removed:
a b c X d d d d d
- Actual:
a b c d d d d d
- Original:
- Check if
p = "abcd"
is subsequence of"abcddddd"
:- Match 'a' at position 0 ✓
- Match 'b' at position 1 ✓
- Match 'c' at position 2 ✓
- Match 'd' at position 3 ✓
- Result: TRUE, so
l = mid = 1
Iteration 3:
l = 1
,r = 2
mid = (1 + 2 + 1) >> 1 = 2
- Check if we can remove first 2 elements:
removable[0:2] = [3, 2]
- Mark positions 3 and 2 as removed:
- Original:
a b c b d d d d d
- Removed:
a b X X d d d d d
- Actual:
a b d d d d d
- Original:
- Check if
p = "abcd"
is subsequence of"abddddd"
:- Match 'a' at position 0 ✓
- Match 'b' at position 1 ✓
- Can't find 'c' ✗
- Result: FALSE, so
r = mid - 1 = 1
Final State:
l = r = 1
- Binary search terminates
Answer: The maximum k = 1
, meaning we can remove at most 1 character (at index 3) while keeping p
as a subsequence of s
.
The beauty of this approach is that we only needed to check 3 different values of k
(1, 2, and 3) instead of all 6 possible values, thanks to binary search exploiting the monotonic property of the problem.
Solution Implementation
1class Solution:
2 def maximumRemovals(self, s: str, p: str, removable: List[int]) -> int:
3 def is_subsequence_after_removal(num_removals: int) -> bool:
4 """
5 Check if p is still a subsequence of s after removing
6 the first num_removals characters from removable array.
7
8 Args:
9 num_removals: Number of characters to remove from s
10
11 Returns:
12 True if p is still a subsequence, False otherwise
13 """
14 # Mark positions to be removed
15 is_removed = [False] * len(s)
16 for index in removable[:num_removals]:
17 is_removed[index] = True
18
19 # Two pointers to check if p is subsequence of modified s
20 s_pointer = 0
21 p_pointer = 0
22
23 while s_pointer < len(s) and p_pointer < len(p):
24 # If current position in s is not removed and matches current char in p
25 if not is_removed[s_pointer] and s[s_pointer] == p[p_pointer]:
26 p_pointer += 1
27 s_pointer += 1
28
29 # Check if we matched all characters in p
30 return p_pointer == len(p)
31
32 # Binary search for the maximum number of removals
33 left, right = 0, len(removable)
34
35 while left < right:
36 # Calculate mid point (using bit shift for division by 2)
37 # Adding 1 before division ensures we round up
38 mid = (left + right + 1) >> 1
39
40 if is_subsequence_after_removal(mid):
41 # If p is still a subsequence, we can try removing more
42 left = mid
43 else:
44 # If p is not a subsequence, we need to remove fewer
45 right = mid - 1
46
47 return left
48
1class Solution {
2 // Instance variables to avoid passing parameters repeatedly
3 private char[] sourceArray;
4 private char[] patternArray;
5 private int[] removableIndices;
6
7 /**
8 * Finds the maximum number of characters that can be removed from string s
9 * while still keeping p as a subsequence of the resulting string.
10 *
11 * @param s The source string
12 * @param p The pattern string that must remain as a subsequence
13 * @param removable Array of indices indicating the order of character removal
14 * @return Maximum number of removals possible
15 */
16 public int maximumRemovals(String s, String p, int[] removable) {
17 // Initialize instance variables
18 this.sourceArray = s.toCharArray();
19 this.patternArray = p.toCharArray();
20 this.removableIndices = removable;
21
22 // Binary search on the answer: find maximum k where first k removals still keep p as subsequence
23 int left = 0;
24 int right = removable.length;
25
26 while (left < right) {
27 // Calculate mid point (using bit shift for division by 2)
28 // Adding 1 before division ensures we round up for proper binary search convergence
29 int mid = (left + right + 1) >> 1;
30
31 // Check if removing first 'mid' characters still keeps p as subsequence
32 if (canRemoveKCharacters(mid)) {
33 // If yes, we can potentially remove more characters
34 left = mid;
35 } else {
36 // If no, we need to remove fewer characters
37 right = mid - 1;
38 }
39 }
40
41 return left;
42 }
43
44 /**
45 * Checks if pattern p is still a subsequence of s after removing
46 * the first k characters from removable array.
47 *
48 * @param k Number of characters to remove
49 * @return true if p remains a subsequence, false otherwise
50 */
51 private boolean canRemoveKCharacters(int k) {
52 // Mark which characters are removed
53 boolean[] isRemoved = new boolean[sourceArray.length];
54 for (int i = 0; i < k; i++) {
55 isRemoved[removableIndices[i]] = true;
56 }
57
58 // Two-pointer approach to check if p is subsequence of modified s
59 int sourceIndex = 0;
60 int patternIndex = 0;
61
62 // Traverse source string and try to match pattern characters
63 while (sourceIndex < sourceArray.length && patternIndex < patternArray.length) {
64 // If current character is not removed and matches pattern character
65 if (!isRemoved[sourceIndex] && patternArray[patternIndex] == sourceArray[sourceIndex]) {
66 // Move to next character in pattern
67 patternIndex++;
68 }
69 // Always move to next character in source
70 sourceIndex++;
71 }
72
73 // Check if we matched all characters in pattern
74 return patternIndex == patternArray.length;
75 }
76}
77
1class Solution {
2public:
3 int maximumRemovals(string s, string p, vector<int>& removable) {
4 int strLen = s.size();
5 int patternLen = p.size();
6
7 // Binary search on the number of removals
8 // left = minimum removals, right = maximum possible removals
9 int left = 0, right = removable.size();
10
11 // Lambda function to check if p is still a subsequence of s
12 // after removing the first k characters from removable array
13 auto isSubsequence = [&](int numRemovals) {
14 // Track which indices are removed
15 bool isRemoved[strLen];
16 memset(isRemoved, false, sizeof(isRemoved));
17
18 // Mark the first numRemovals indices as removed
19 for (int i = 0; i < numRemovals; i++) {
20 isRemoved[removable[i]] = true;
21 }
22
23 // Check if p is still a subsequence of s
24 int strIdx = 0; // Pointer for string s
25 int patternIdx = 0; // Pointer for pattern p
26
27 while (strIdx < strLen && patternIdx < patternLen) {
28 // If current character is not removed and matches pattern
29 if (!isRemoved[strIdx] && s[strIdx] == p[patternIdx]) {
30 patternIdx++;
31 }
32 strIdx++;
33 }
34
35 // Return true if all characters of p were matched
36 return patternIdx == patternLen;
37 };
38
39 // Binary search to find maximum k where p is still subsequence
40 while (left < right) {
41 // Use upper mid to avoid infinite loop
42 int mid = (left + right + 1) / 2;
43
44 if (isSubsequence(mid)) {
45 // If p is still subsequence, we can try removing more
46 left = mid;
47 } else {
48 // If p is not subsequence, we need to remove fewer
49 right = mid - 1;
50 }
51 }
52
53 return left;
54 }
55};
56
1/**
2 * Finds the maximum number of characters that can be removed from string s
3 * while still keeping p as a subsequence of the resulting string.
4 * Uses binary search to find the optimal number of removals.
5 *
6 * @param s - The source string from which characters will be removed
7 * @param p - The pattern string that must remain as a subsequence
8 * @param removable - Array of indices indicating the order of character removals from s
9 * @returns The maximum k such that after removing the first k characters from removable, p is still a subsequence of s
10 */
11function maximumRemovals(s: string, p: string, removable: number[]): number {
12 const sourceLength: number = s.length;
13 const patternLength: number = p.length;
14
15 // Binary search boundaries: we can remove between 0 and removable.length characters
16 let left: number = 0;
17 let right: number = removable.length;
18
19 // Array to track which positions in s have been removed
20 const isRemoved: boolean[] = new Array(sourceLength);
21
22 /**
23 * Checks if pattern p is still a subsequence of s after removing
24 * the first k characters from the removable array
25 *
26 * @param k - Number of characters to remove from s
27 * @returns True if p is still a subsequence after removals, false otherwise
28 */
29 const isPatternSubsequence = (k: number): boolean => {
30 // Reset the removal tracking array
31 isRemoved.fill(false);
32
33 // Mark the first k positions as removed
34 for (let i = 0; i < k; i++) {
35 isRemoved[removable[i]] = true;
36 }
37
38 // Two-pointer approach to check if p is a subsequence of modified s
39 let sourceIndex: number = 0;
40 let patternIndex: number = 0;
41
42 while (sourceIndex < sourceLength && patternIndex < patternLength) {
43 // If current character is not removed and matches pattern character
44 if (!isRemoved[sourceIndex] && s[sourceIndex] === p[patternIndex]) {
45 patternIndex++;
46 }
47 sourceIndex++;
48 }
49
50 // Pattern is a subsequence if we matched all its characters
51 return patternIndex === patternLength;
52 };
53
54 // Binary search for the maximum number of removals
55 while (left < right) {
56 // Calculate mid point (using bit shift for integer division)
57 // Adding 1 ensures we round up to avoid infinite loop
58 const mid: number = (left + right + 1) >> 1;
59
60 if (isPatternSubsequence(mid)) {
61 // If we can remove mid characters, try removing more
62 left = mid;
63 } else {
64 // If we cannot remove mid characters, try removing fewer
65 right = mid - 1;
66 }
67 }
68
69 return left;
70}
71
Time and Space Complexity
The time complexity is O(k × n × log k)
, where n
is the length of string s
and k
is the length of removable
.
- The binary search performs
O(log k)
iterations since we're searching in the range[0, len(removable)]
- For each binary search iteration, we call the
check
function - The
check
function:- Creates a boolean array
rem
of sizen
and marksmid
positions:O(n + mid)
wheremid ≤ k
- Performs a two-pointer traversal through string
s
to verify ifp
is a subsequence:O(n)
- Overall
check
function:O(n)
- Creates a boolean array
- Total time complexity:
O(log k) × O(n) = O(n × log k)
However, the reference answer states O(k × log k)
. This appears to be considering a different perspective or possibly an error. The actual complexity should be O(n × log k)
based on the code analysis.
The space complexity is O(n)
, where n
is the length of string s
.
- The
rem
boolean array in thecheck
function usesO(n)
space - Other variables use constant space
- Total space complexity:
O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Binary Search Boundary Update
One of the most common mistakes is using the wrong binary search template, particularly with the mid calculation and boundary updates.
Pitfall Code:
# Wrong approach that can cause infinite loop while left < right: mid = (left + right) // 2 # Missing +1 if is_subsequence_after_removal(mid): left = mid # This can cause infinite loop else: right = mid - 1
Issue: When left = 2
and right = 3
, mid
will always be 2, and if the check passes, left
stays at 2, creating an infinite loop.
Solution: Use mid = (left + right + 1) // 2
to bias toward the upper half when searching for the maximum valid value.
2. Off-by-One Error in Binary Search Range
Another pitfall is incorrectly setting the initial range or returning the wrong value.
Pitfall Code:
# Wrong: Using len(removable) - 1 as upper bound
left, right = 0, len(removable) - 1
# Or returning the wrong value
return left + 1 # Wrong adjustment
Issue: The range should be [0, len(removable)]
inclusive because we can remove anywhere from 0 to all indices in the removable array.
Solution: Initialize right = len(removable)
and return left
directly.
3. Inefficient Subsequence Checking
Creating a new string after removals is inefficient and can lead to TLE (Time Limit Exceeded).
Pitfall Code:
def is_subsequence_after_removal(num_removals):
# Inefficient: Creating new string
modified_s = ""
removed_set = set(removable[:num_removals])
for i in range(len(s)):
if i not in removed_set:
modified_s += s[i]
# Then check if p is subsequence of modified_s
j = 0
for char in modified_s:
if j < len(p) and char == p[j]:
j += 1
return j == len(p)
Issue: String concatenation in Python creates new strings each time, making this O(n²) for string building alone.
Solution: Use a boolean array to mark removed positions and check subsequence without creating a new string, keeping the time complexity at O(n).
4. Using Set Instead of Boolean Array
While using a set seems intuitive, it can be less efficient for this problem.
Pitfall Code:
def is_subsequence_after_removal(num_removals):
removed_indices = set(removable[:num_removals])
s_pointer, p_pointer = 0, 0
while s_pointer < len(s) and p_pointer < len(p):
if s_pointer not in removed_indices and s[s_pointer] == p[p_pointer]:
p_pointer += 1
s_pointer += 1
return p_pointer == len(p)
Issue: Set lookup is O(1) average case but has overhead. For index checking with known bounds, a boolean array is faster and more cache-friendly.
Solution: Use a boolean array for O(1) guaranteed lookup with better cache locality.
Given a sorted array of integers and an integer called target, find the element that
equals to the target and return its index. Select the correct code that fills the
___
in the given code snippet.
1def binary_search(arr, target):
2 left, right = 0, len(arr) - 1
3 while left ___ right:
4 mid = (left + right) // 2
5 if arr[mid] == target:
6 return mid
7 if arr[mid] < target:
8 ___ = mid + 1
9 else:
10 ___ = mid - 1
11 return -1
12
1public static int binarySearch(int[] arr, int target) {
2 int left = 0;
3 int right = arr.length - 1;
4
5 while (left ___ right) {
6 int mid = left + (right - left) / 2;
7 if (arr[mid] == target) return mid;
8 if (arr[mid] < target) {
9 ___ = mid + 1;
10 } else {
11 ___ = mid - 1;
12 }
13 }
14 return -1;
15}
16
1function binarySearch(arr, target) {
2 let left = 0;
3 let right = arr.length - 1;
4
5 while (left ___ right) {
6 let mid = left + Math.trunc((right - left) / 2);
7 if (arr[mid] == target) return mid;
8 if (arr[mid] < target) {
9 ___ = mid + 1;
10 } else {
11 ___ = mid - 1;
12 }
13 }
14 return -1;
15}
16
Recommended Readings
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
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!