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:leftis the minimum index among all removed charactersrightis 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
sas a subsequence - The kept suffix must match some suffix of
sas a subsequence - These matches in
smust not overlap
This leads us to precompute two important pieces of information:
- For each position
iint, what's the earliest position inswhere we can matcht[0...i]as a subsequence? Let's call thisf[i]. - For each position
jint, what's the latest position inswhere 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 Implementation
1from math import inf
2
3class Solution:
4 def minimumScore(self, s: str, t: str) -> int:
5 """
6 Find the minimum length of substring to remove from t
7 so that the remaining parts form a subsequence of s.
8 """
9
10 def can_remove_length(removal_length):
11 """
12 Check if we can remove a substring of given length from t
13 and still have the remaining parts be a subsequence of s.
14 """
15 for start_pos in range(target_len):
16 left_end = start_pos - 1
17 right_start = start_pos + removal_length
18
19 last_s_pos_for_left = left_match_positions[left_end] if left_end >= 0 else -1
20 first_s_pos_for_right = right_match_positions[right_start] if right_start < target_len else source_len + 1
21
22 if last_s_pos_for_left < first_s_pos_for_right:
23 return True
24 return False
25
26 source_len, target_len = len(s), len(t)
27
28 # left_match_positions[i]: rightmost index in s where t[0:i+1] can be matched
29 left_match_positions = [inf] * target_len
30
31 # right_match_positions[i]: leftmost index in s where t[i:] can be matched
32 right_match_positions = [-1] * target_len
33
34 # Build left_match_positions: match t from left to right in s
35 s_idx, t_idx = 0, 0
36 while s_idx < source_len and t_idx < target_len:
37 if s[s_idx] == t[t_idx]:
38 left_match_positions[t_idx] = s_idx
39 t_idx += 1
40 s_idx += 1
41
42 # Build right_match_positions: match t from right to left in s
43 s_idx, t_idx = source_len - 1, target_len - 1
44 while s_idx >= 0 and t_idx >= 0:
45 if s[s_idx] == t[t_idx]:
46 right_match_positions[t_idx] = s_idx
47 t_idx -= 1
48 s_idx -= 1
49
50 # Binary search for minimum removal length
51 # Feasible condition: can_remove_length returns True
52 left, right = 0, target_len
53 first_true_index = -1
54
55 while left <= right:
56 mid = (left + right) // 2
57 if can_remove_length(mid):
58 first_true_index = mid
59 right = mid - 1
60 else:
61 left = mid + 1
62
63 return first_true_index
641class Solution {
2 private int sLength;
3 private int tLength;
4 private int[] leftmostPositions;
5 private int[] rightmostPositions;
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;
16 rightmostPositions[i] = -1;
17 }
18
19 // Build leftmostPositions array
20 int tIndex = 0;
21 for (int sIndex = 0; sIndex < sLength && tIndex < tLength; sIndex++) {
22 if (s.charAt(sIndex) == t.charAt(tIndex)) {
23 leftmostPositions[tIndex] = sIndex;
24 tIndex++;
25 }
26 }
27
28 // Build rightmostPositions array
29 tIndex = tLength - 1;
30 for (int sIndex = sLength - 1; sIndex >= 0 && tIndex >= 0; sIndex--) {
31 if (s.charAt(sIndex) == t.charAt(tIndex)) {
32 rightmostPositions[tIndex] = sIndex;
33 tIndex--;
34 }
35 }
36
37 // Binary search for minimum deletion length
38 // Feasible condition: can delete substring of this length
39 int left = 0;
40 int right = tLength;
41 int firstTrueIndex = -1;
42
43 while (left <= right) {
44 int mid = left + (right - left) / 2;
45 if (canDeleteSubstring(mid)) {
46 firstTrueIndex = mid;
47 right = mid - 1;
48 } else {
49 left = mid + 1;
50 }
51 }
52
53 return firstTrueIndex;
54 }
55
56 private boolean canDeleteSubstring(int deletionLength) {
57 for (int startPos = 0; startPos < tLength; startPos++) {
58 int prefixEnd = startPos - 1;
59 int suffixStart = startPos + deletionLength;
60
61 int prefixMaxPosition = (prefixEnd >= 0) ? leftmostPositions[prefixEnd] : -1;
62 int suffixMinPosition = (suffixStart < tLength) ? rightmostPositions[suffixStart] : sLength + 1;
63
64 if (prefixMaxPosition < suffixMinPosition) {
65 return true;
66 }
67 }
68
69 return false;
70 }
71}
721class Solution {
2public:
3 int minimumScore(string s, string t) {
4 int sLength = s.size();
5 int tLength = t.size();
6
7 vector<int> leftmostMatch(tLength, 1e6);
8 vector<int> rightmostMatch(tLength, -1);
9
10 // Build leftmostMatch array
11 for (int sIndex = 0, tIndex = 0; sIndex < sLength && tIndex < tLength; ++sIndex) {
12 if (s[sIndex] == t[tIndex]) {
13 leftmostMatch[tIndex] = sIndex;
14 ++tIndex;
15 }
16 }
17
18 // Build rightmostMatch array
19 for (int sIndex = sLength - 1, tIndex = tLength - 1; sIndex >= 0 && tIndex >= 0; --sIndex) {
20 if (s[sIndex] == t[tIndex]) {
21 rightmostMatch[tIndex] = sIndex;
22 --tIndex;
23 }
24 }
25
26 auto canRemoveLength = [&](int removeLength) {
27 for (int startPos = 0; startPos < tLength; ++startPos) {
28 int leftEnd = startPos - 1;
29 int rightStart = startPos + removeLength;
30
31 int leftBoundary = (leftEnd >= 0) ? leftmostMatch[leftEnd] : -1;
32 int rightBoundary = (rightStart < tLength) ? rightmostMatch[rightStart] : sLength + 1;
33
34 if (leftBoundary < rightBoundary) {
35 return true;
36 }
37 }
38 return false;
39 };
40
41 // Binary search for minimum removal length
42 // Feasible condition: can remove substring of this length
43 int left = 0, right = tLength;
44 int firstTrueIndex = -1;
45
46 while (left <= right) {
47 int mid = left + (right - left) / 2;
48 if (canRemoveLength(mid)) {
49 firstTrueIndex = mid;
50 right = mid - 1;
51 } else {
52 left = mid + 1;
53 }
54 }
55
56 return firstTrueIndex;
57 }
58};
591function minimumScore(s: string, t: string): number {
2 const sLength: number = s.length;
3 const tLength: number = t.length;
4
5 const leftmostMatch: number[] = new Array(tLength).fill(1e6);
6 const rightmostMatch: number[] = new Array(tLength).fill(-1);
7
8 // Build leftmostMatch array
9 let tIndex: number = 0;
10 for (let sIndex = 0; sIndex < sLength && tIndex < tLength; sIndex++) {
11 if (s[sIndex] === t[tIndex]) {
12 leftmostMatch[tIndex] = sIndex;
13 tIndex++;
14 }
15 }
16
17 // Build rightmostMatch array
18 tIndex = tLength - 1;
19 for (let sIndex = sLength - 1; sIndex >= 0 && tIndex >= 0; sIndex--) {
20 if (s[sIndex] === t[tIndex]) {
21 rightmostMatch[tIndex] = sIndex;
22 tIndex--;
23 }
24 }
25
26 const canRemoveLength = (removeLength: number): boolean => {
27 for (let startPos = 0; startPos < tLength; startPos++) {
28 const leftEnd: number = startPos - 1;
29 const rightStart: number = startPos + removeLength;
30
31 const leftBoundary: number = (leftEnd >= 0) ? leftmostMatch[leftEnd] : -1;
32 const rightBoundary: number = (rightStart < tLength) ? rightmostMatch[rightStart] : sLength + 1;
33
34 if (leftBoundary < rightBoundary) {
35 return true;
36 }
37 }
38 return false;
39 };
40
41 // Binary search for minimum removal length
42 // Feasible condition: can remove substring of this length
43 let left: number = 0;
44 let right: number = tLength;
45 let firstTrueIndex: number = -1;
46
47 while (left <= right) {
48 const mid: number = Math.floor((left + right) / 2);
49 if (canRemoveLength(mid)) {
50 firstTrueIndex = mid;
51 right = mid - 1;
52 } else {
53 left = mid + 1;
54 }
55 }
56
57 return firstTrueIndex;
58}
59Solution 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 5-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] = 1t[1] = 'z': No 'z' insafter matching 'b', sof[1] = inft[2] = 'a': Would need 'bza' but 'z' doesn't exist, sof[2] = inft[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' insis at position 6, sog[3] = 6t[2] = 'a': Need "aa", can start at position 4 (matches positions 4,6), sog[2] = 4t[1] = 'z': Need "zaa", but 'z' doesn't exist ins, sog[1] = -1t[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
sto 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).
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_leftperforms a binary search on the range[0, n], which hasn + 1elements. This results inO(log n)iterations. For each iteration, thecheckfunction is called, which loops throughnelements (fromk = 0tok = n-1), performingO(1)operations in each iteration. Therefore, eachcheckcall 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
fof sizen:O(n)space - Array
gof 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. Using Wrong Binary Search Template
Pitfall: Using while left < right with right = mid instead of the standard template. This can lead to confusion and potential bugs.
Solution: Use the standard binary search template with while left <= right, first_true_index tracking, and right = mid - 1:
left, right = 0, target_len first_true_index = -1 while left <= right: mid = (left + right) // 2 if feasible(mid): first_true_index = mid right = mid - 1 else: left = mid + 1 return first_true_index
2. Incorrect Boundary Handling in Preprocessing Arrays
Pitfall: 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 remain at their initial values (inf and -1). This could lead to incorrect comparisons in the validation function.
Solution: The validation function correctly handles these extreme values by checking boundary conditions before accessing the arrays.
3. 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
4. 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.
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.
Is the following code DFS or BFS?
void search(Node root) { if (!root) return; visit(root); root.visited = true; for (Node node in root.adjacent) { if (!node.visited) { search(node); } } }
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
https assets algo monster cover_photos Binary_Search svg Binary Search Intuition Binary search is an efficient array search algorithm It works by narrowing down the search range by half each time If you have looked up a word in a physical dictionary you've already used binary search in real life Let's
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!