3316. Find Maximum Removals From Source String
Problem Description
You have a string source
of length n
, a string pattern
that is a subsequence of source
, and a sorted array targetIndices
containing distinct integers in the range [0, n-1]
.
An operation consists of removing a character at index idx
from source
where:
idx
must be an element intargetIndices
- After removing the character,
pattern
must still remain a subsequence ofsource
When you remove a character, the indices of other characters don't change. For example, if you remove the character at index 1 from "acb"
, the character 'b'
remains at index 2.
The goal is to find the maximum number of operations (character removals) you can perform while ensuring pattern
stays a subsequence of source
.
Example walkthrough:
- If
source = "abcab"
,pattern = "ab"
, andtargetIndices = [0, 1, 2]
- You could potentially remove the character at index 2 (
'c'
), andpattern = "ab"
would still be a subsequence of the resulting"abab"
- But you cannot remove characters at indices 0 or 1 without breaking the subsequence requirement
The solution uses dynamic programming where f[i][j]
represents the maximum number of deletions possible when considering the first i
characters of source
and matching the first j
characters of pattern
. At each position, you decide whether to:
- Skip the current character (and potentially delete it if its index is in
targetIndices
) - Match the current character with the pattern (if they're equal)
The answer is f[m][n]
where m
is the length of source
and n
is the length of pattern
.
Intuition
The key insight is that we want to maximize deletions while maintaining pattern
as a subsequence. This naturally leads to a dynamic programming approach where we track both our progress through source
and pattern
.
Think of it this way: as we scan through source
, at each character we face a decision - should we use this character to match with pattern
, or should we skip it (and potentially delete it)? The challenge is that we need to ensure we can still form the complete pattern
subsequence after our deletions.
The critical observation is that we need to track two things simultaneously:
- How many characters of
source
we've processed - How many characters of
pattern
we've successfully matched
This leads us to define our state as f[i][j]
- the maximum deletions possible when we've looked at the first i
characters of source
and matched the first j
characters of pattern
.
At each position (i, j)
, we have two choices:
Choice 1: Skip the current character in source
- We move from
f[i-1][j]
tof[i][j]
(processed one more character but didn't match any new pattern character) - If this skipped character's index is in
targetIndices
, we can delete it, adding 1 to our deletion count
Choice 2: Match the current character (if possible)
- Only available when
source[i-1] == pattern[j-1]
- We move from
f[i-1][j-1]
tof[i][j]
(processed one more source character AND matched one more pattern character) - We don't delete this character since we're using it for matching
The beauty of this approach is that by considering both options at each step and taking the maximum, we automatically find the optimal strategy that maximizes deletions while ensuring the pattern remains a subsequence. The final answer f[m][n]
gives us the maximum deletions when we've processed all of source
and matched all of pattern
.
Learn more about Two Pointers and Dynamic Programming patterns.
Solution Approach
We implement the dynamic programming solution using a 2D array f
where f[i][j]
represents the maximum number of deletions possible when considering the first i
characters of source
and matching the first j
characters of pattern
.
Initialization:
- Create a 2D array
f
of size(m+1) × (n+1)
wherem = len(source)
andn = len(pattern)
- Initialize all values to
-inf
(negative infinity) to represent invalid states - Set
f[0][0] = 0
as the base case (no characters processed, no deletions made) - Convert
targetIndices
to a sets
for O(1) lookup time
State Transition:
For each character at position i
in source
(1-indexed) and each possible matched position j
in pattern
(0 to n
):
-
Skip current character in source:
f[i][j] = f[i-1][j] + int((i-1) in s)
- We inherit the state from
f[i-1][j]
(same pattern progress, one more source character processed) - If index
i-1
(0-indexed) is intargetIndices
, we can delete it, adding 1 to our count
- We inherit the state from
-
Match current character (if possible):
if j > 0 and source[i-1] == pattern[j-1]: f[i][j] = max(f[i][j], f[i-1][j-1])
- Only applicable when
j > 0
(we have pattern characters to match) - Only when characters match:
source[i-1] == pattern[j-1]
- We take the maximum of skipping vs matching to get the optimal choice
- Only applicable when
Why this works:
- By considering both options (skip/delete vs match) at each step, we explore all valid ways to form the pattern subsequence
- The
-inf
initialization ensures invalid states (where we can't form the required subsequence) are never chosen - The maximum operation in the matching case ensures we pick the strategy that maximizes deletions
Final Answer:
Return f[m][n]
, which represents the maximum deletions when we've processed all m
characters of source
and successfully matched all n
characters of pattern
.
Time Complexity: O(m × n) where m and n are the lengths of source and pattern Space Complexity: O(m × n) for the DP table
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 trace through a small example to understand how the dynamic programming solution works.
Input:
source = "aba"
pattern = "aa"
targetIndices = [0, 2]
We need to find the maximum number of characters we can delete from indices 0 and 2 while keeping "aa" as a subsequence.
Setup:
m = 3
(length of source),n = 2
(length of pattern)- Create DP table
f[4][3]
initialized with-inf
- Set
f[0][0] = 0
(base case) - Convert targetIndices to set:
s = {0, 2}
DP Table Evolution:
Initial state:
j=0 j=1 j=2 i=0 0 -inf -inf i=1 -inf -inf -inf i=2 -inf -inf -inf i=3 -inf -inf -inf
Processing i=1 (source[0]='a'):
For j=0: Skip 'a'
f[1][0] = f[0][0] + 1 = 0 + 1 = 1
(index 0 is in targetIndices, so +1)
For j=1: Can match 'a' with pattern[0]='a'
- Skip option:
f[1][1] = f[0][1] + 1 = -inf + 1 = -inf
- Match option:
f[1][1] = f[0][0] = 0
- Take max:
f[1][1] = 0
Processing i=2 (source[1]='b'):
For j=0: Skip 'b'
f[2][0] = f[1][0] + 0 = 1 + 0 = 1
(index 1 not in targetIndices)
For j=1: 'b' ≠ 'a', can only skip
f[2][1] = f[1][1] + 0 = 0 + 0 = 0
For j=2: Cannot match (would need pattern[1]='a' but source[1]='b')
- Skip option:
f[2][2] = f[1][2] + 0 = -inf
Processing i=3 (source[2]='a'):
For j=0: Skip 'a'
f[3][0] = f[2][0] + 1 = 1 + 1 = 2
(index 2 is in targetIndices)
For j=1: Can match 'a' with pattern[0]='a'
- Skip option:
f[3][1] = f[2][1] + 1 = 0 + 1 = 1
- Match option:
f[3][1] = f[2][0] = 1
- Take max:
f[3][1] = 1
For j=2: Can match 'a' with pattern[1]='a'
- Skip option:
f[3][2] = f[2][2] + 1 = -inf
- Match option:
f[3][2] = f[2][1] = 0
- Take max:
f[3][2] = 0
Final DP Table:
j=0 j=1 j=2 i=0 0 -inf -inf i=1 1 0 -inf i=2 1 0 -inf i=3 2 1 0
Answer: f[3][2] = 0
This means we can perform 0 deletions while maintaining "aa" as a subsequence. Why? Because we need both 'a' characters (at indices 0 and 2) to form the pattern "aa", so we cannot delete either of them even though they're in targetIndices.
Verification:
- Original: "aba" contains subsequence "aa" (using indices 0 and 2)
- If we delete index 0: "ba" doesn't contain "aa" ❌
- If we delete index 2: "ab" doesn't contain "aa" ❌
- Maximum deletions = 0 ✓
Solution Implementation
1class Solution:
2 def maxRemovals(self, source: str, pattern: str, targetIndices: List[int]) -> int:
3 source_length, pattern_length = len(source), len(pattern)
4
5 # dp[i][j] = maximum removals possible using first i chars of source
6 # to match first j chars of pattern
7 dp = [[float('-inf')] * (pattern_length + 1) for _ in range(source_length + 1)]
8
9 # Base case: empty source matches empty pattern with 0 removals
10 dp[0][0] = 0
11
12 # Convert target indices to set for O(1) lookup
13 removable_indices = set(targetIndices)
14
15 # Iterate through each character in source
16 for source_idx in range(1, source_length + 1):
17 current_char = source[source_idx - 1]
18
19 # Try matching different lengths of pattern
20 for pattern_idx in range(pattern_length + 1):
21 # Option 1: Skip current source character
22 # If current index (0-based) is removable, increment removal count
23 dp[source_idx][pattern_idx] = dp[source_idx - 1][pattern_idx]
24 if (source_idx - 1) in removable_indices:
25 dp[source_idx][pattern_idx] += 1
26
27 # Option 2: Match current source char with pattern char
28 # Only if we have pattern chars to match and characters match
29 if pattern_idx > 0 and current_char == pattern[pattern_idx - 1]:
30 dp[source_idx][pattern_idx] = max(
31 dp[source_idx][pattern_idx],
32 dp[source_idx - 1][pattern_idx - 1]
33 )
34
35 # Return maximum removals while matching entire pattern
36 return dp[source_length][pattern_length]
37
1class Solution {
2 public int maxRemovals(String source, String pattern, int[] targetIndices) {
3 int sourceLength = source.length();
4 int patternLength = pattern.length();
5
6 // dp[i][j] represents the maximum removals possible
7 // when considering first i characters of source and matching first j characters of pattern
8 int[][] dp = new int[sourceLength + 1][patternLength + 1];
9
10 // Initialize with negative infinity to mark invalid states
11 final int NEGATIVE_INFINITY = Integer.MAX_VALUE / 2;
12 for (int[] row : dp) {
13 Arrays.fill(row, -NEGATIVE_INFINITY);
14 }
15
16 // Base case: empty source and empty pattern, no removals
17 dp[0][0] = 0;
18
19 // Mark removable positions in source string
20 // removableFlags[i] = 1 if index i can be removed, 0 otherwise
21 int[] removableFlags = new int[sourceLength];
22 for (int index : targetIndices) {
23 removableFlags[index] = 1;
24 }
25
26 // Fill the DP table
27 for (int i = 1; i <= sourceLength; ++i) {
28 for (int j = 0; j <= patternLength; ++j) {
29 // Option 1: Skip current character in source
30 // If it's removable, increment the removal count
31 dp[i][j] = dp[i - 1][j] + removableFlags[i - 1];
32
33 // Option 2: Match current characters if they are equal
34 // This means we use source[i-1] to match pattern[j-1]
35 if (j > 0 && source.charAt(i - 1) == pattern.charAt(j - 1)) {
36 dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - 1]);
37 }
38 }
39 }
40
41 // Return maximum removals when entire source is processed
42 // and entire pattern is matched
43 return dp[sourceLength][patternLength];
44 }
45}
46
1class Solution {
2public:
3 int maxRemovals(string source, string pattern, vector<int>& targetIndices) {
4 int sourceLen = source.length();
5 int patternLen = pattern.length();
6
7 // dp[i][j] represents the maximum number of characters we can remove
8 // from the first i characters of source while matching the first j characters of pattern
9 // Initialize with negative infinity to mark invalid states
10 vector<vector<int>> dp(sourceLen + 1, vector<int>(patternLen + 1, INT_MIN / 2));
11
12 // Base case: empty source and empty pattern, no removals
13 dp[0][0] = 0;
14
15 // Create a boolean array to mark which indices are removable
16 // isRemovable[i] = 1 if index i is in targetIndices, 0 otherwise
17 vector<int> isRemovable(sourceLen, 0);
18 for (int index : targetIndices) {
19 isRemovable[index] = 1;
20 }
21
22 // Fill the DP table
23 for (int i = 1; i <= sourceLen; ++i) {
24 for (int j = 0; j <= patternLen; ++j) {
25 // Option 1: Skip current character in source (and remove it if possible)
26 // If current character (at index i-1) is removable, increment the count
27 dp[i][j] = dp[i - 1][j] + isRemovable[i - 1];
28
29 // Option 2: Match current character with pattern if possible
30 // Only valid if we have pattern characters to match and characters match
31 if (j > 0 && source[i - 1] == pattern[j - 1]) {
32 // Take maximum of skipping or matching
33 dp[i][j] = max(dp[i][j], dp[i - 1][j - 1]);
34 }
35 }
36 }
37
38 // Return the maximum removals when we've processed all of source
39 // and matched all of pattern
40 return dp[sourceLen][patternLen];
41 }
42};
43
1/**
2 * Finds the maximum number of characters that can be removed from source
3 * while still keeping pattern as a subsequence
4 * @param source - The source string to remove characters from
5 * @param pattern - The pattern string that must remain as a subsequence
6 * @param targetIndices - Array of indices in source that are candidates for removal
7 * @returns Maximum number of removable characters
8 */
9function maxRemovals(source: string, pattern: string, targetIndices: number[]): number {
10 const sourceLength: number = source.length;
11 const patternLength: number = pattern.length;
12
13 // dp[i][j] represents the maximum number of removable characters
14 // when considering first i characters of source and first j characters of pattern
15 const dp: number[][] = Array.from(
16 { length: sourceLength + 1 },
17 () => Array(patternLength + 1).fill(-Infinity)
18 );
19
20 // Base case: empty source and empty pattern, no removals needed
21 dp[0][0] = 0;
22
23 // Mark which indices in source are removable (1 if removable, 0 otherwise)
24 const isRemovable: number[] = Array(sourceLength).fill(0);
25 for (const index of targetIndices) {
26 isRemovable[index] = 1;
27 }
28
29 // Fill the DP table
30 for (let i = 1; i <= sourceLength; i++) {
31 for (let j = 0; j <= patternLength; j++) {
32 // Option 1: Skip current character in source
33 // Add to removal count if current character is removable
34 dp[i][j] = dp[i - 1][j] + isRemovable[i - 1];
35
36 // Option 2: Match current characters if they are equal
37 if (j > 0 && source[i - 1] === pattern[j - 1]) {
38 // Take maximum of skipping or matching
39 dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - 1]);
40 }
41 }
42 }
43
44 // Return the maximum removals when both strings are fully processed
45 return dp[sourceLength][patternLength];
46}
47
Time and Space Complexity
The time complexity is O(m × n)
, where m
is the length of the source
string and n
is the length of the pattern
string. This is because the code uses two nested loops - the outer loop iterates through each character in source
(m iterations), and the inner loop iterates through each possible position in pattern
(n+1 iterations). Each operation inside the nested loops takes constant time O(1)
, including the set lookup operation (i - 1) in s
and the comparison operations.
The space complexity is O(m × n)
. The dominant space usage comes from the 2D dynamic programming table f
, which has dimensions (m + 1) × (n + 1)
. Additionally, the code creates a set s
from targetIndices
, which takes O(k)
space where k
is the length of targetIndices
, but since k ≤ m
, this doesn't affect the overall space complexity of O(m × n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Initialization of Invalid States
Pitfall: Initializing the DP table with 0 instead of negative infinity, which can lead to accepting invalid subsequence formations.
Why it's problematic: If you initialize with 0, states that cannot form a valid subsequence would appear valid, potentially propagating incorrect values through the DP transitions.
Example of incorrect code:
# WRONG: Initializing with 0
dp = [[0] * (pattern_length + 1) for _ in range(source_length + 1)]
Solution: Always initialize with negative infinity except for the base case:
# CORRECT: Initialize with -inf, then set base case
dp = [[float('-inf')] * (pattern_length + 1) for _ in range(source_length + 1)]
dp[0][0] = 0
2. Index Confusion Between 0-based and 1-based
Pitfall: Mixing up the indexing when checking if an index is in targetIndices
or when accessing characters from strings.
Why it's problematic: The DP table uses 1-based indexing for convenience (where dp[i][j]
represents first i
characters), but strings and targetIndices
use 0-based indexing.
Example of incorrect code:
# WRONG: Using source_idx directly instead of source_idx - 1 if source_idx in removable_indices: # This checks wrong index! dp[source_idx][pattern_idx] += 1
Solution: Always convert to 0-based when checking targetIndices or accessing string characters:
# CORRECT: Convert to 0-based index if (source_idx - 1) in removable_indices: dp[source_idx][pattern_idx] += 1 # When accessing characters: current_char = source[source_idx - 1] # 0-based indexing
3. Forgetting to Handle the "Skip Without Deletion" Case
Pitfall: Only incrementing the removal count when skipping a character, without considering that some characters can be skipped but not deleted (if their index isn't in targetIndices
).
Why it's problematic: The transition should always allow skipping a character; we just don't increment the deletion count if the index isn't removable.
Example of incorrect code:
# WRONG: Only updating when index is removable if (source_idx - 1) in removable_indices: dp[source_idx][pattern_idx] = dp[source_idx - 1][pattern_idx] + 1 # Missing the else case!
Solution: Always inherit from the previous state, then conditionally add 1:
# CORRECT: Always update, conditionally increment dp[source_idx][pattern_idx] = dp[source_idx - 1][pattern_idx] if (source_idx - 1) in removable_indices: dp[source_idx][pattern_idx] += 1
4. Not Converting targetIndices to a Set
Pitfall: Using the list directly for membership checking, resulting in O(k) lookup time where k is the size of targetIndices.
Why it's problematic: This increases the overall time complexity from O(m×n) to O(m×n×k), which can cause timeouts for large inputs.
Example of incorrect code:
# WRONG: Using list for membership check if (source_idx - 1) in targetIndices: # O(k) operation dp[source_idx][pattern_idx] += 1
Solution: Convert to set once at the beginning:
# CORRECT: Convert to set for O(1) lookup
removable_indices = set(targetIndices)
if (source_idx - 1) in removable_indices: # O(1) operation
dp[source_idx][pattern_idx] += 1
Which of these pictures shows the visit order of a depth-first search?
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
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
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!