Facebook Pixel

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 where 0 <= 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 of s
  • If k = 1: remove s[0] ('a'), resulting in "bcab", p is still a subsequence
  • If k = 2: remove s[0] and s[1] ('a' and 'b'), resulting in "cab", p is still a subsequence
  • If k = 3: remove s[0], s[1], and s[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.

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

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:

  1. Create a boolean array rem of size len(s) to mark which characters are removed
  2. Mark the first k indices from removable as True in the rem array
  3. Use two pointers to check if p is a subsequence:
    • Pointer i traverses through string s
    • Pointer j traverses through string p
    • If s[i] is not removed (rem[i] is False) and matches p[j], increment j
    • Always increment i to move through s
  4. Return True if we've matched all characters of p (i.e., j == len(p))

Binary Search Process: While l < r:

  1. 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
  2. Check if removing the first mid characters still maintains the subsequence:
    • If check(mid) returns True: We can potentially remove more, so update l = mid
    • If check(mid) returns False: We've removed too many, so update r = mid - 1

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 Evaluator

Example 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
  • 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
  • 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
  • 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 size n and marks mid positions: O(n + mid) where mid ≤ k
    • Performs a two-pointer traversal through string s to verify if p is a subsequence: O(n)
    • Overall check function: O(n)
  • 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 the check function uses O(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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

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

Load More