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 will also keep p as a subsequence. Conversely, if removing k characters breaks the subsequence relationship, then removing more characters will also break it.

This monotonic behavior creates a sequence of feasibility values:

Removals k:   0    1    2    3    4    5    6
Feasible:     T    T    T    T    F    F    F
                        first_true_index (searching from right)

We want to find the maximum k where p remains a subsequence. Using our standard binary search template, we search for the last true value - the maximum feasible k.

For each candidate k, we define feasible(k) as: after removing characters at indices removable[0] through removable[k-1], is p still a subsequence of s? We check this using two pointers to match characters of p against the non-removed characters of s.

The search range is from 0 (remove nothing) to len(removable) (remove all), and we track the maximum feasible value with first_true_index.

Learn more about Two Pointers and Binary Search patterns.

Solution Implementation

1class Solution:
2    def maximumRemovals(self, s: str, p: str, removable: List[int]) -> int:
3        def feasible(k: int) -> bool:
4            """Check if p is still a subsequence after removing first k indices."""
5            is_removed = [False] * len(s)
6            for i in range(k):
7                is_removed[removable[i]] = True
8
9            # Two pointers to check subsequence
10            j = 0
11            for i in range(len(s)):
12                if not is_removed[i] and j < len(p) and s[i] == p[j]:
13                    j += 1
14            return j == len(p)
15
16        # Binary search with standard template
17        left, right = 0, len(removable)
18        first_true_index = 0
19
20        while left <= right:
21            mid = (left + right) // 2
22
23            if feasible(mid):
24                first_true_index = mid
25                left = mid + 1
26            else:
27                right = mid - 1
28
29        return first_true_index
30
1class Solution {
2    private char[] source;
3    private char[] pattern;
4    private int[] removable;
5
6    public int maximumRemovals(String s, String p, int[] removable) {
7        this.source = s.toCharArray();
8        this.pattern = p.toCharArray();
9        this.removable = removable;
10
11        // Binary search with standard template
12        int left = 0;
13        int right = removable.length;
14        int firstTrueIndex = 0;
15
16        while (left <= right) {
17            int mid = left + (right - left) / 2;
18
19            if (feasible(mid)) {
20                firstTrueIndex = mid;
21                left = mid + 1;
22            } else {
23                right = mid - 1;
24            }
25        }
26
27        return firstTrueIndex;
28    }
29
30    private boolean feasible(int k) {
31        boolean[] isRemoved = new boolean[source.length];
32        for (int i = 0; i < k; i++) {
33            isRemoved[removable[i]] = true;
34        }
35
36        // Two pointers to check subsequence
37        int j = 0;
38        for (int i = 0; i < source.length && j < pattern.length; i++) {
39            if (!isRemoved[i] && source[i] == pattern[j]) {
40                j++;
41            }
42        }
43        return j == pattern.length;
44    }
45}
46
1class Solution {
2public:
3    int maximumRemovals(string s, string p, vector<int>& removable) {
4        int n = s.size();
5        int m = p.size();
6
7        auto feasible = [&](int k) {
8            vector<bool> isRemoved(n, false);
9            for (int i = 0; i < k; i++) {
10                isRemoved[removable[i]] = true;
11            }
12
13            // Two pointers to check subsequence
14            int j = 0;
15            for (int i = 0; i < n && j < m; i++) {
16                if (!isRemoved[i] && s[i] == p[j]) {
17                    j++;
18                }
19            }
20            return j == m;
21        };
22
23        // Binary search with standard template
24        int left = 0;
25        int right = removable.size();
26        int firstTrueIndex = 0;
27
28        while (left <= right) {
29            int mid = left + (right - left) / 2;
30
31            if (feasible(mid)) {
32                firstTrueIndex = mid;
33                left = mid + 1;
34            } else {
35                right = mid - 1;
36            }
37        }
38
39        return firstTrueIndex;
40    }
41};
42
1function maximumRemovals(s: string, p: string, removable: number[]): number {
2    const feasible = (k: number): boolean => {
3        const isRemoved: boolean[] = new Array(s.length).fill(false);
4        for (let i = 0; i < k; i++) {
5            isRemoved[removable[i]] = true;
6        }
7
8        // Two pointers to check subsequence
9        let j = 0;
10        for (let i = 0; i < s.length && j < p.length; i++) {
11            if (!isRemoved[i] && s[i] === p[j]) {
12                j++;
13            }
14        }
15        return j === p.length;
16    };
17
18    // Binary search with standard template
19    let left = 0;
20    let right = removable.length;
21    let firstTrueIndex = 0;
22
23    while (left <= right) {
24        const mid = Math.floor((left + right) / 2);
25
26        if (feasible(mid)) {
27            firstTrueIndex = mid;
28            left = mid + 1;
29        } else {
30            right = mid - 1;
31        }
32    }
33
34    return firstTrueIndex;
35}
36

Solution Approach

The solution uses binary search with the standard template, combined with a subsequence checking function.

Template Structure:

left, right = 0, len(removable)
first_true_index = 0  # Default: we can remove 0 characters

while left <= right:
    mid = (left + right) // 2
    if feasible(mid):
        first_true_index = mid  # Record this feasible k
        left = mid + 1          # Try larger k
    else:
        right = mid - 1         # Try smaller k

return first_true_index

The Feasibility Function: For a given value k, we verify if p remains a subsequence of s after removing the first k characters:

  1. Create a boolean array to mark which positions are removed
  2. Mark positions removable[0] through removable[k-1] as removed
  3. Use two pointers to check if p is a subsequence:
    • Pointer i traverses string s
    • Pointer j traverses string p
    • If s[i] is not removed and matches p[j], increment j
    • Always increment i
  4. Return True if j == len(p) (all characters matched)

Binary Search Logic:

  • If feasible: Record first_true_index = mid and search right (left = mid + 1)
  • If not feasible: Search left (right = mid - 1)

Time Complexity: O(n × log m) where n is the length of s and m is the length of removable.

Space Complexity: O(n) for the boolean array.

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 with s = "abcbddddd", p = "abcd", and removable = [3, 2, 1, 4, 5, 6].

Step 1: Initialize

  • left = 0, right = 6 (length of removable)
  • first_true_index = 0

Step 2: Binary search iterations

Iteration 1: left = 0, right = 6

  • mid = (0 + 6) // 2 = 3
  • Remove first 3 indices: positions 3, 2, 1
  • String becomes: a _ _ _ d d d d d → "addddd"
  • Is "abcd" a subsequence? Can match 'a', but can't find 'b' ✗
  • Update: right = 3 - 1 = 2

Iteration 2: left = 0, right = 2

  • mid = (0 + 2) // 2 = 1
  • Remove first 1 index: position 3
  • String becomes: a b c _ d d d d d → "abcddddd"
  • Is "abcd" a subsequence? Match a→b→c→d ✓
  • Update: first_true_index = 1, left = 1 + 1 = 2

Iteration 3: left = 2, right = 2

  • mid = (2 + 2) // 2 = 2
  • Remove first 2 indices: positions 3 and 2
  • String becomes: a b _ _ d d d d d → "abddddd"
  • Is "abcd" a subsequence? Match a→b, but can't find 'c' ✗
  • Update: right = 2 - 1 = 1

Step 3: Loop terminates

  • left = 2 > right = 1
  • Return first_true_index = 1

We can remove at most 1 character while keeping p as a subsequence.

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. Using the Wrong Binary Search Template

The Pitfall: Using while left < right with left = mid instead of the standard while left <= right template.

Why It Happens: The left < right with left = mid variant requires mid = (left + right + 1) // 2 to avoid infinite loops. The standard template is simpler and less error-prone.

Solution: Use the standard template:

left, right = 0, len(removable)
first_true_index = 0

while left <= right:
    mid = (left + right) // 2
    if feasible(mid):
        first_true_index = mid
        left = mid + 1
    else:
        right = mid - 1

return first_true_index

2. Off-by-One Error in Binary Search Range

The Pitfall: Using len(removable) - 1 as upper bound.

Solution: The range should be [0, len(removable)] because we can remove 0 to all indices.

3. Inefficient Subsequence Checking

The Pitfall: Creating a new string after removals.

Solution: Use a boolean array to mark removed positions and check subsequence in-place:

is_removed = [False] * len(s)
for i in range(k):
    is_removed[removable[i]] = True
Loading...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Consider the classic dynamic programming of longest increasing subsequence:

Find the length of the longest subsequence of a given sequence such that all elements of the subsequence are sorted in increasing order.

For example, the length of LIS for [50, 3, 10, 7, 40, 80] is 4 and LIS is [3, 7, 40, 80].

What is the recurrence relation?


Recommended Readings

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

Load More