2565. Subsequence With the Minimum Score
Problem Description
You have two strings s
and t
. Your goal is to make t
a subsequence of s
by removing some characters from t
.
When you remove characters from t
, you incur a score based on the positions of the removed characters:
- If you don't remove any characters, the score is
0
- If you do remove characters, the score is calculated as
right - left + 1
, where:left
is the minimum index among all removed charactersright
is the maximum index among all removed characters
Your task is to find the minimum possible score needed to make t
a subsequence of s
.
A subsequence means that the remaining characters in t
(after removal) appear in s
in the same relative order, though not necessarily consecutively. For example, "ace"
is a subsequence of "abcde"
because you can find a
, c
, and e
in that order within "abcde"
.
The key insight is that when you remove characters from t
, you're essentially removing a contiguous substring (from index left
to right
). The remaining parts of t
(the prefix before left
and the suffix after right
) must then form a subsequence of s
. The challenge is to find the optimal substring to remove that minimizes the score right - left + 1
.
Intuition
The key observation is that when we remove characters from t
, we're actually removing a contiguous substring. This means we keep a prefix of t
(before the removed part) and a suffix of t
(after the removed part), and both these parts need to be subsequences of s
.
Think about it this way: if we remove characters from index left
to right
in t
, we're left with:
t[0...left-1]
(the prefix we keep)t[right+1...n-1]
(the suffix we keep)
For t
to become a subsequence of s
after this removal, we need:
- The kept prefix must match some prefix of
s
as a subsequence - The kept suffix must match some suffix of
s
as a subsequence - These matches in
s
must not overlap
This leads us to precompute two important pieces of information:
- For each position
i
int
, what's the earliest position ins
where we can matcht[0...i]
as a subsequence? Let's call thisf[i]
. - For each position
j
int
, what's the latest position ins
where we can start matchingt[j...n-1]
as a subsequence? Let's call thisg[j]
.
With these precomputed values, for any removal range [left, right]
, we can quickly check if it's valid: we keep prefix t[0...left-1]
which needs s[0...f[left-1]]
, and suffix t[right+1...n-1]
which needs s[g[right+1]...m-1]
. The removal is valid if f[left-1] < g[right+1]
(no overlap in s
).
Since we want to minimize the score right - left + 1
, which is essentially the length of the removed substring, and longer removals are always valid if shorter ones are valid (monotonicity property), we can use binary search on the length of the removal to find the minimum valid length.
Learn more about Two Pointers and Binary Search patterns.
Solution Approach
The solution consists of three main phases: preprocessing, binary search, and validation.
Phase 1: Preprocessing Arrays f
and g
First, we build array f
where f[i]
stores the minimum index in string s
needed to match the prefix t[0...i]
as a subsequence:
f = [inf] * n # Initialize with infinity i, j = 0, 0 while i < m and j < n: if s[i] == t[j]: f[j] = i # Record position in s where t[j] is matched j += 1 i += 1
This greedy approach matches each character of t
with the earliest possible character in s
.
Similarly, we build array g
where g[i]
stores the maximum starting index in string s
from where we can match the suffix t[i...n-1]
as a subsequence:
g = [-1] * n # Initialize with -1 i, j = m - 1, n - 1 while i >= 0 and j >= 0: if s[i] == t[j]: g[j] = i # Record position in s where t[j] is matched j -= 1 i -= 1
This processes from right to left, matching each character of t
with the latest possible character in s
.
Phase 2: Binary Search on Removal Length
Since removing more characters always makes it easier to form a subsequence (monotonicity property), we can binary search on the length of the substring to remove:
return bisect_left(range(n + 1), True, key=check)
This searches for the minimum length x
(from 0 to n) where removing a substring of length x
makes t
a subsequence of s
.
Phase 3: Validation Function
The check(x)
function determines if we can remove a substring of length x
from t
and still form a subsequence of s
:
def check(x):
for k in range(n):
i, j = k - 1, k + x
l = f[i] if i >= 0 else -1
r = g[j] if j < n else m + 1
if l < r:
return True
return False
For each possible starting position k
of removal:
- We keep prefix
t[0...k-1]
which requiress[0...f[k-1]]
- We keep suffix
t[k+x...n-1]
which requiress[g[k+x]...m-1]
- The removal is valid if
f[k-1] < g[k+x]
(no overlap ins
)
The boundary cases are handled by:
- If
i < 0
(no prefix), setl = -1
- If
j >= n
(no suffix), setr = m + 1
The binary search finds the minimum valid removal length, which is our answer.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example with s = "abacaba"
and t = "bzaa"
.
Step 1: Build the prefix array f
We need to find for each position in t
, the earliest position in s
where we can match t[0...i]
as a subsequence.
t[0] = 'b'
: Found ats[1]
, sof[0] = 1
t[1] = 'z'
: No 'z' ins
after matching 'b', sof[1] = inf
t[2] = 'a'
: Would need 'bza' but 'z' doesn't exist, sof[2] = inf
t[3] = 'a'
: Would need 'bzaa' but 'z' doesn't exist, sof[3] = inf
Result: f = [1, inf, inf, inf]
Step 2: Build the suffix array g
We need to find for each position in t
, the latest starting position in s
where we can match t[i...n-1]
as a subsequence.
Working backwards:
t[3] = 'a'
: Last 'a' ins
is at position 6, sog[3] = 6
t[2] = 'a'
: Need "aa", can start at position 4 (matches positions 4,6), sog[2] = 4
t[1] = 'z'
: Need "zaa", but 'z' doesn't exist ins
, sog[1] = -1
t[0] = 'b'
: Need "bzaa", but can't match 'z', sog[0] = -1
Result: g = [-1, -1, 4, 6]
Step 3: Binary search on removal length
We try different removal lengths to find the minimum:
Check length 0 (no removal):
- Keep entire string "bzaa"
- Need
s
to contain "bzaa" as subsequence - From
g[0] = -1
, we know this is impossible - Result: FALSE
Check length 1 (remove 1 character):
Try removing at each position:
- Remove position 0: Keep suffix "zaa", need
g[1] = -1
(impossible) - Remove position 1: Keep prefix "b" and suffix "aa"
- Prefix "b" needs
s[0...f[0]] = s[0...1]
- Suffix "aa" needs
s[g[2]...6] = s[4...6]
- Check:
f[0] < g[2]
→1 < 4
✓ Valid!
- Prefix "b" needs
- Remove position 2: Keep prefix "bz", but
f[1] = inf
(impossible) - Remove position 3: Keep prefix "bza", but
f[2] = inf
(impossible)
Result: TRUE (we found a valid removal of length 1)
Answer: The minimum score is 1 (removing one character at position 1).
We can verify: removing 'z' from "bzaa" gives us "baa", which is indeed a subsequence of "abacaba" (taking the 'b' at position 1 and the 'a's at positions 2 and 4).
Solution Implementation
1from math import inf
2from bisect import bisect_left
3
4class Solution:
5 def minimumScore(self, s: str, t: str) -> int:
6 """
7 Find the minimum length of substring to remove from t
8 so that the remaining parts form a subsequence of s.
9 """
10
11 def can_remove_length(removal_length):
12 """
13 Check if we can remove a substring of given length from t
14 and still have the remaining parts be a subsequence of s.
15 """
16 # Try all possible starting positions for removal
17 for start_pos in range(target_len):
18 left_end = start_pos - 1 # Last index of left part
19 right_start = start_pos + removal_length # First index of right part
20
21 # Get the rightmost position in s where left part ends
22 last_s_pos_for_left = left_match_positions[left_end] if left_end >= 0 else -1
23
24 # Get the leftmost position in s where right part starts
25 first_s_pos_for_right = right_match_positions[right_start] if right_start < target_len else source_len + 1
26
27 # If left part ends before right part starts in s, this removal works
28 if last_s_pos_for_left < first_s_pos_for_right:
29 return True
30 return False
31
32 source_len, target_len = len(s), len(t)
33
34 # left_match_positions[i]: rightmost index in s where t[0:i+1] can be matched as subsequence
35 left_match_positions = [inf] * target_len
36
37 # right_match_positions[i]: leftmost index in s where t[i:] can be matched as subsequence
38 right_match_positions = [-1] * target_len
39
40 # Build left_match_positions: match t from left to right in s
41 s_idx, t_idx = 0, 0
42 while s_idx < source_len and t_idx < target_len:
43 if s[s_idx] == t[t_idx]:
44 left_match_positions[t_idx] = s_idx
45 t_idx += 1
46 s_idx += 1
47
48 # Build right_match_positions: match t from right to left in s
49 s_idx, t_idx = source_len - 1, target_len - 1
50 while s_idx >= 0 and t_idx >= 0:
51 if s[s_idx] == t[t_idx]:
52 right_match_positions[t_idx] = s_idx
53 t_idx -= 1
54 s_idx -= 1
55
56 # Binary search for minimum removal length that makes it possible
57 return bisect_left(range(target_len + 1), True, key=can_remove_length)
58
1class Solution {
2 private int sLength;
3 private int tLength;
4 private int[] leftmostPositions; // leftmostPositions[i] = earliest position in s where t[0..i] can be matched
5 private int[] rightmostPositions; // rightmostPositions[i] = latest position in s where t[i..n-1] can be matched
6
7 public int minimumScore(String s, String t) {
8 sLength = s.length();
9 tLength = t.length();
10 leftmostPositions = new int[tLength];
11 rightmostPositions = new int[tLength];
12
13 // Initialize arrays with extreme values
14 for (int i = 0; i < tLength; i++) {
15 leftmostPositions[i] = Integer.MAX_VALUE / 2; // Large value indicating no match found
16 rightmostPositions[i] = -1; // -1 indicating no match found
17 }
18
19 // Build leftmostPositions array: find earliest positions in s to match prefix of t
20 // leftmostPositions[j] = earliest index in s where t[0..j] can be matched as subsequence
21 int tIndex = 0;
22 for (int sIndex = 0; sIndex < sLength && tIndex < tLength; sIndex++) {
23 if (s.charAt(sIndex) == t.charAt(tIndex)) {
24 leftmostPositions[tIndex] = sIndex;
25 tIndex++;
26 }
27 }
28
29 // Build rightmostPositions array: find latest positions in s to match suffix of t
30 // rightmostPositions[j] = latest index in s where t[j..n-1] can be matched as subsequence
31 tIndex = tLength - 1;
32 for (int sIndex = sLength - 1; sIndex >= 0 && tIndex >= 0; sIndex--) {
33 if (s.charAt(sIndex) == t.charAt(tIndex)) {
34 rightmostPositions[tIndex] = sIndex;
35 tIndex--;
36 }
37 }
38
39 // Binary search for minimum deletion length
40 int left = 0;
41 int right = tLength;
42 while (left < right) {
43 int mid = (left + right) / 2;
44 if (canDeleteSubstring(mid)) {
45 right = mid; // Try smaller deletion length
46 } else {
47 left = mid + 1; // Need larger deletion length
48 }
49 }
50
51 return left;
52 }
53
54 /**
55 * Check if we can delete a substring of given length from t
56 * such that remaining parts can be subsequences of s
57 * @param deletionLength the length of substring to delete from t
58 * @return true if deletion is possible, false otherwise
59 */
60 private boolean canDeleteSubstring(int deletionLength) {
61 // Try deleting substring t[startPos..endPos-1] where endPos - startPos = deletionLength
62 for (int startPos = 0; startPos < tLength; startPos++) {
63 int prefixEnd = startPos - 1; // Last index of prefix to keep
64 int suffixStart = startPos + deletionLength; // First index of suffix to keep
65
66 // Get the rightmost position in s where prefix can end
67 int prefixMaxPosition = (prefixEnd >= 0) ? leftmostPositions[prefixEnd] : -1;
68
69 // Get the leftmost position in s where suffix can start
70 int suffixMinPosition = (suffixStart < tLength) ? rightmostPositions[suffixStart] : sLength + 1;
71
72 // Check if prefix can be placed before suffix in s (non-overlapping)
73 if (prefixMaxPosition < suffixMinPosition) {
74 return true;
75 }
76 }
77
78 return false;
79 }
80}
81
1class Solution {
2public:
3 int minimumScore(string s, string t) {
4 int sLength = s.size();
5 int tLength = t.size();
6
7 // leftmostMatch[i] stores the leftmost index in s where t[0...i] can be matched as subsequence
8 vector<int> leftmostMatch(tLength, 1e6);
9
10 // rightmostMatch[i] stores the rightmost index in s where t[i...n-1] can be matched as subsequence
11 vector<int> rightmostMatch(tLength, -1);
12
13 // Build leftmostMatch array: match t's prefix with s from left to right
14 for (int sIndex = 0, tIndex = 0; sIndex < sLength && tIndex < tLength; ++sIndex) {
15 if (s[sIndex] == t[tIndex]) {
16 leftmostMatch[tIndex] = sIndex;
17 ++tIndex;
18 }
19 }
20
21 // Build rightmostMatch array: match t's suffix with s from right to left
22 for (int sIndex = sLength - 1, tIndex = tLength - 1; sIndex >= 0 && tIndex >= 0; --sIndex) {
23 if (s[sIndex] == t[tIndex]) {
24 rightmostMatch[tIndex] = sIndex;
25 --tIndex;
26 }
27 }
28
29 // Check if we can remove a substring of given length from t
30 // and still match the remaining parts with s as subsequences
31 auto canRemoveLength = [&](int removeLength) {
32 // Try removing substring t[startPos...startPos+removeLength-1]
33 for (int startPos = 0; startPos < tLength; ++startPos) {
34 int leftEnd = startPos - 1; // Last index of left part
35 int rightStart = startPos + removeLength; // First index of right part
36
37 // Get the rightmost position in s where left part ends
38 int leftBoundary = (leftEnd >= 0) ? leftmostMatch[leftEnd] : -1;
39
40 // Get the leftmost position in s where right part starts
41 int rightBoundary = (rightStart < tLength) ? rightmostMatch[rightStart] : sLength + 1;
42
43 // Check if left part can be matched before right part in s
44 if (leftBoundary < rightBoundary) {
45 return true;
46 }
47 }
48 return false;
49 };
50
51 // Binary search for minimum removal length
52 int left = 0, right = tLength;
53 while (left < right) {
54 int mid = (left + right) >> 1;
55 if (canRemoveLength(mid)) {
56 right = mid; // Can remove mid characters, try smaller
57 } else {
58 left = mid + 1; // Cannot remove mid characters, need more
59 }
60 }
61
62 return left;
63 }
64};
65
1function minimumScore(s: string, t: string): number {
2 const sLength: number = s.length;
3 const tLength: number = t.length;
4
5 // leftmostMatch[i] stores the leftmost index in s where t[0...i] can be matched as subsequence
6 const leftmostMatch: number[] = new Array(tLength).fill(1e6);
7
8 // rightmostMatch[i] stores the rightmost index in s where t[i...n-1] can be matched as subsequence
9 const rightmostMatch: number[] = new Array(tLength).fill(-1);
10
11 // Build leftmostMatch array: match t's prefix with s from left to right
12 let tIndex: number = 0;
13 for (let sIndex = 0; sIndex < sLength && tIndex < tLength; sIndex++) {
14 if (s[sIndex] === t[tIndex]) {
15 leftmostMatch[tIndex] = sIndex;
16 tIndex++;
17 }
18 }
19
20 // Build rightmostMatch array: match t's suffix with s from right to left
21 tIndex = tLength - 1;
22 for (let sIndex = sLength - 1; sIndex >= 0 && tIndex >= 0; sIndex--) {
23 if (s[sIndex] === t[tIndex]) {
24 rightmostMatch[tIndex] = sIndex;
25 tIndex--;
26 }
27 }
28
29 // Check if we can remove a substring of given length from t
30 // and still match the remaining parts with s as subsequences
31 const canRemoveLength = (removeLength: number): boolean => {
32 // Try removing substring t[startPos...startPos+removeLength-1]
33 for (let startPos = 0; startPos < tLength; startPos++) {
34 const leftEnd: number = startPos - 1; // Last index of left part
35 const rightStart: number = startPos + removeLength; // First index of right part
36
37 // Get the rightmost position in s where left part ends
38 const leftBoundary: number = (leftEnd >= 0) ? leftmostMatch[leftEnd] : -1;
39
40 // Get the leftmost position in s where right part starts
41 const rightBoundary: number = (rightStart < tLength) ? rightmostMatch[rightStart] : sLength + 1;
42
43 // Check if left part can be matched before right part in s
44 if (leftBoundary < rightBoundary) {
45 return true;
46 }
47 }
48 return false;
49 };
50
51 // Binary search for minimum removal length
52 let left: number = 0;
53 let right: number = tLength;
54 while (left < right) {
55 const mid: number = Math.floor((left + right) / 2);
56 if (canRemoveLength(mid)) {
57 right = mid; // Can remove mid characters, try smaller
58 } else {
59 left = mid + 1; // Cannot remove mid characters, need more
60 }
61 }
62
63 return left;
64}
65
Time and Space Complexity
Time Complexity: O(n × log n)
The algorithm consists of three main parts:
-
First forward pass to build array
f
: This iterates through strings
(lengthm
) and stringt
(lengthn
) once, takingO(m + n)
time. -
Second backward pass to build array
g
: Similarly iterates through both strings once, takingO(m + n)
time. -
Binary search with check function: The
bisect_left
performs a binary search on the range[0, n]
, which hasn + 1
elements. This results inO(log n)
iterations. For each iteration, thecheck
function is called, which loops throughn
elements (fromk = 0
tok = n-1
), performingO(1)
operations in each iteration. Therefore, eachcheck
call takesO(n)
time.
The total time complexity is: O(m + n) + O(m + n) + O(n × log n) = O(m + n + n × log n)
Since we're typically looking for the subsequence t
in string s
where n ≤ m
, and the dominant term is n × log n
, the overall time complexity simplifies to O(n × log n)
.
Space Complexity: O(n)
The algorithm uses:
- Array
f
of sizen
:O(n)
space - Array
g
of sizen
:O(n)
space - A few integer variables (
i
,j
,k
,l
,r
, etc.):O(1)
space
The total space complexity is: O(n) + O(n) + O(1) = O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Boundary Handling in Preprocessing Arrays
Pitfall: A common mistake is not properly handling cases where the entire prefix or suffix of t
cannot be matched in s
. If t
cannot be fully matched as a subsequence of s
, the preprocessing arrays may contain uninitialized or incorrect values.
Example: If s = "abc"
and t = "xyz"
, none of the characters match, so the arrays f
and g
remain at their initial values (inf
and -1
). This could lead to incorrect comparisons in the validation function.
Solution: After building the arrays, check if any position remains unmatched (still inf
in f
or -1
in g
). These values indicate that the corresponding prefix/suffix cannot be matched:
# After building left_match_positions
for i in range(target_len):
if left_match_positions[i] == inf:
# Characters from index i onwards cannot be matched as prefix
# We must remove at least from index i to the end
break
# After building right_match_positions
for i in range(target_len - 1, -1, -1):
if right_match_positions[i] == -1:
# Characters from index 0 to i cannot be matched as suffix
# We must remove at least from index 0 to i
break
2. Off-by-One Errors in Index Calculations
Pitfall: When calculating the removal range, it's easy to make off-by-one errors. For instance, if we remove characters from index k
to k + x - 1
(total x
characters), the remaining suffix starts at index k + x
, not k + x - 1
.
Example: If t = "abcd"
and we remove 2 characters starting at index 1, we remove indices 1 and 2 ("bc"
), leaving "a"
(index 0) and "d"
(index 3).
Solution: Be explicit about index ranges and use clear variable names:
def can_remove_length(removal_length):
for removal_start in range(target_len):
removal_end = removal_start + removal_length - 1 # inclusive end
prefix_end = removal_start - 1 # last index of prefix (-1 if no prefix)
suffix_start = removal_end + 1 # first index of suffix (>= target_len if no suffix)
# Now the logic is clearer
left_bound = left_match_positions[prefix_end] if prefix_end >= 0 else -1
right_bound = right_match_positions[suffix_start] if suffix_start < target_len else source_len
if left_bound < right_bound:
return True
return False
3. Misunderstanding the Score Calculation
Pitfall: The score is right - left + 1
where left
and right
are indices in t
, not in s
. Some might mistakenly use indices from s
or forget the +1
in the formula.
Example: Removing characters at indices 2, 3, 4 from t
gives score = 4 - 2 + 1 = 3
, not 2
.
Solution: Remember that we're always removing a contiguous substring from t
, and the score is simply the length of that substring. The binary search directly searches for this length, making the score calculation implicit and avoiding confusion.
4. Not Handling Edge Cases
Pitfall: Failing to consider edge cases like:
- Empty strings
- When
t
is already a subsequence ofs
(removal length = 0) - When entire
t
needs to be removed (removal length = len(t))
Solution: Ensure the binary search range includes both 0 and len(t)
:
# Correct: includes both 0 (no removal) and target_len (remove everything)
return bisect_left(range(target_len + 1), True, key=can_remove_length)
# Also validate edge cases explicitly
if target_len == 0: # Empty t is always a subsequence
return 0
if source_len == 0: # Empty s means we must remove all of t
return target_len
Which of the following array represent a max heap?
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!