Facebook Pixel

1898. Maximum Number of Removable Characters

MediumArrayTwo PointersStringBinary Search
LeetCode ↗

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?

How We Pick the Algorithm

Why Binary Search?

This problem maps to Binary Search through a short path in the full flowchart.

Graph?noSorted ormonotonic?yesBinary Search

If k removals leave p as a subsequence, so does any smaller k, so binary search the largest k while checking subsequence membership with a marked-removed pass.

Open in Flowchart

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.

Pattern Learn more about Two Pointers and Binary Search patterns.

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.

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.

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

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)

Pattern 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

Ready to land your dream job?

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

Start Evaluator
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