Subsequence Matching (Earliest-Match Two Pointers)
Is Subsequence (LC 392)(opens leetcode.com)
You get two strings, s and t. Decide whether s is a subsequence of t: whether every character of s appears somewhere in t, in the same relative order, though not necessarily next to each other. Return true or false.
a, b and c appear in t at positions 0, 2 and 5, in that order.Greedy Subsequence Match
i -> s, j -> t (claim s[i] only on a match, i reaches len(s))| 1 | def isSubsequence(s: str, t: str) -> bool: |
| 2 | """Is Subsequence: walk t once with j, and advance i in s only when the current characters match. |
| 3 | Time: O(len(t)) | Space: O(1) |
| 4 | """ |
| 5 | i = 0 # Characters of s already claimed, in order |
| 6 | for j in range(len(t)): |
| 7 | if i < len(s) and s[i] == t[j]: |
| 8 | i += 1 # The earliest available match for s[i] in t never costs a later match |
| 9 | Â |
| 10 | return i == len(s) |
t[0..j] has been walked, i is exactly the number of characters of s claimed so far, and s[0..i-1] already appears, in order, inside t[0..j].i counts how many characters of s are already claimed, in order; walking t once with j, advance i only when t[j] equals s[i], the next character of s still unclaimed. s is a subsequence of t exactly when i reaches len(s) by the time the walk ends. Claiming the earliest copy of s[i] in t is always safe: every character of t after that point is still free for the rest of s, so no earlier choice can ever cost a later match, and a later copy would only leave less of t behind for what comes next. The trap is advancing i on every character of t instead of only on a match: i += 1 outside the if claims characters that were never actually matched, so the final check i == len(s) can pass even when s is not really a subsequence of t.
See it run
Is Subsequence: isSubsequence("abc", "ahbgdcxy")
i 0j -
The top row is t and the bottom row is s. Cells are in string order, and each cell shows one character.
jthe character of t that is read nowithe next character of s that is not claimed yetclaimeda character of s that is already matched, in ordermatcha character of t that was used to claim a character of s
Start: s is "abc" and t is "ahbgdcxy". i is 0, so no character of s is claimed yet.
Gotcha 1 of 4
- The trap
Gotcha 1: Advancing
ion every character oft, not only on a match (the card's trap)Mistakefor j in range(len(t)): i += 1Fixfor j in range(len(t)): if i < len(s) and s[i] == t[j]: i += 1What happens:
iis supposed to count only the characters ofsthat were actually matched int, in order. Movingi += 1outside theifmakesicount every character oftinstead.ithen reacheslen(s)as soon astis at least as long ass, no matter what characters either string holds, so the final checki == len(s)saysTruefor pairs that are not a real subsequence match.Example: s = "abc", t = "ahbgdc"Mistake: returns FalseFix: returns True
- Both strings get to skip characters, and the goal is the length of the best alignment between them, as in Longest Common Subsequence (LC 1143): that needs a DP table over every pair of prefixes, not a single one-pass claim where only one string is ever allowed to skip. See Two-Sequence DP
- The two pointers start at opposite ends of one string and move toward each other comparing characters for equality, as in Valid Palindrome (LC 125), instead of one pointer claiming characters of a second, separate string. See Converging Pointers
Deep Dive
Why the earliest match is always safe
A subsequence keeps the order of the characters but may skip characters. So s is a subsequence of t when the characters of s can be found in t from left to right.
The variable i counts the characters of s that are already claimed. The next character to claim is s[i]. The loop reads t once with j. When t[j] equals s[i], the code claims it and does i += 1.
Taking the first copy of s[i] in t never loses a later match. Every character of t after j is still unread, so it is still free for s[i + 1] and the rest of s. A later copy of the same character would leave fewer characters of t for the rest of s. So the earliest copy is at least as good as any other copy.
The code has no search and no table. This card is its own root: it is not a merge of two sequences. Only t is read in full, and only s is allowed to stay at the same position.
Tracing s = "abc" in t = "ahbgdcxy"
The board shows t in the top row and s in the bottom row. The tag i is the next character of s that is not claimed. The tag j is the character of t that is read now.
t[0] is a and matches s[0], so i becomes 1. Then t[1] is h, and s[1] is b. They differ, so i stays 1. This is the step that the trap changes: i moves only on a match.
t[2] is b and matches, so i becomes 2. t[3] is g and t[4] is d. Neither equals c, so i stays 2. t[5] is c and matches, so i becomes 3.
Now i equals len(s), which is 3. The last two characters of t, x and y, are read and skipped. The final check i == len(s) returns True. This is a longer input than the first example of Is Subsequence (LC 392), where s is abc and t is ahbgdc. The result is the same.
What the final check and the cost say
i can only grow by one at a time, and only on a match. So i == len(s) is true exactly when every character of s was claimed in order. When the loop ends with a smaller i, the characters from s[i] onward were not found after the previous match. For s = "axc" and t = "ahbgdc", the answer is False, because x does not appear in t.
An empty s returns True, because i starts at 0 and len(s) is 0. An empty t with a non-empty s returns False.
The loop reads each character of t once and never moves backward. The time is O(len(t)). The code stores only i and j, so the extra space is O(1).
If the problem gives many strings s for the same t, this loop would read t again for every s. A common change is to store, for each character, the sorted list of its positions in t, and to find the next position with a binary search. That is a different card; this one needs only one string s.
60-Second Pitch
"I keep a counter i of the characters of s that I have claimed. I read t once with j. When t[j] equals s[i], I claim it with i += 1. Otherwise I skip t[j] and i stays. Taking the earliest match is safe. All later characters of t stay free for the rest of s, so no earlier choice can cost a later match. At the end, s is a subsequence of t exactly when i == len(s). The time is O(len(t)) and the extra space is O(1)."
Common Questions
- Why is taking the earliest match correct?
After the earliest copy of
s[i]int, every later character oftis still unread. Any other copy ofs[i]is at the same position or later, so it leaves the same or fewer characters for the rest ofs. No answer that uses a later copy can succeed when the earliest copy fails.- What is the time and space complexity?
The time is O(len(t)), because
jvisits each character oftonce. The extra space isO(1), because the code stores onlyiandj.- Why does this not need dynamic programming?
Only
tmay skip characters in an unlimited way, and the choice at each match is forced to be the earliest one. So there is one possible path, and a single pass finds it. Longest Common Subsequence (LC 1143) lets both strings skip and asks for the best length, so it needs a table.- What changes when there are many strings
sfor onet? Reading
tagain for eachscosts O(len(t)) per query. Store, for each character, the sorted positions int. For each character ofs, binary search the first position after the previous match. This is the follow-up of Is Subsequence (LC 392).- What does the code return for an empty
s? It returns
True.istarts at 0,len(s)is 0, and the checki == len(s)is true before any character oftis read.
SWE Reasoning
Spot it
- The problem asks whether every character of one string appears, in the same order, inside a second string, and the second string may be read with skips.
- The answer is a yes or no, not a count or a best alignment.
- One pass over the longer string with
O(1)extra space is expected.
The invariant, step by step
i = 0. No character ofsis claimed yet.- The loop reads
t[j]forjfrom 0 tolen(t) - 1. - After
t[0..j]is read,s[0..i-1]already appears, in order, insidet[0..j]. - If
i < len(s)ands[i] == t[j], the code claimss[i]withi += 1. The earliest copy leaves every later character oftfree, so the invariant holds. - If the characters differ,
istays the same andt[j]is skipped. The invariant holds. return i == len(s)is true exactly when all ofswas claimed.
Traps
i += 1on every character oft.ithen counts the characters oft, and the final check passes for pairs that are not a subsequence match.i == len(s) - 1.ireacheslen(s)after the last claim, so the check rejects a correct full match.- Returning
Truewhen the loop reaches the last index oft. Reading all oftdoes not mean all ofswas claimed. ch in tfor each character ofs. This ignores the order. Fors = "ba"andt = "ab", both characters exist int, but not in the order ofs.- Reading
s[i]wheni == len(s). Checki < len(s)first, or the index is out of range.
Practice
Subsequence Matching (Earliest-Match Two Pointers)
Is Subsequence (LC 392)(opens leetcode.com)
You get two strings, s and t. Decide whether s is a subsequence of t: whether every character of s appears somewhere in t, in the same relative order, though not necessarily next to each other. Return true or false.
a, b and c appear in t at positions 0, 2 and 5, in that order.Greedy Subsequence Match
i -> s, j -> t (claim s[i] only on a match, i reaches len(s))| 1 | def isSubsequence(s: str, t: str) -> bool: |
| 2 | """Is Subsequence: walk t once with j, and advance i in s only when the current characters match. |
| 3 | Time: O(len(t)) | Space: O(1) |
| 4 | """ |
| 5 | i = 0 # Characters of s already claimed, in order |
| 6 | for j in range(len(t)): |
| 7 | if i < len(s) and s[i] == t[j]: |
| 8 | i += 1 # The earliest available match for s[i] in t never costs a later match |
| 9 | Â |
| 10 | return i == len(s) |
t[0..j] has been walked, i is exactly the number of characters of s claimed so far, and s[0..i-1] already appears, in order, inside t[0..j].i counts how many characters of s are already claimed, in order; walking t once with j, advance i only when t[j] equals s[i], the next character of s still unclaimed. s is a subsequence of t exactly when i reaches len(s) by the time the walk ends. Claiming the earliest copy of s[i] in t is always safe: every character of t after that point is still free for the rest of s, so no earlier choice can ever cost a later match, and a later copy would only leave less of t behind for what comes next. The trap is advancing i on every character of t instead of only on a match: i += 1 outside the if claims characters that were never actually matched, so the final check i == len(s) can pass even when s is not really a subsequence of t.
See it run
Is Subsequence: isSubsequence("abc", "ahbgdcxy")
i 0j -
The top row is t and the bottom row is s. Cells are in string order, and each cell shows one character.
jthe character of t that is read nowithe next character of s that is not claimed yetclaimeda character of s that is already matched, in ordermatcha character of t that was used to claim a character of s
Start: s is "abc" and t is "ahbgdcxy". i is 0, so no character of s is claimed yet.
Gotcha 1 of 4
- The trap
Gotcha 1: Advancing
ion every character oft, not only on a match (the card's trap)Mistakefor j in range(len(t)): i += 1Fixfor j in range(len(t)): if i < len(s) and s[i] == t[j]: i += 1What happens:
iis supposed to count only the characters ofsthat were actually matched int, in order. Movingi += 1outside theifmakesicount every character oftinstead.ithen reacheslen(s)as soon astis at least as long ass, no matter what characters either string holds, so the final checki == len(s)saysTruefor pairs that are not a real subsequence match.Example: s = "abc", t = "ahbgdc"Mistake: returns FalseFix: returns True
- Both strings get to skip characters, and the goal is the length of the best alignment between them, as in Longest Common Subsequence (LC 1143): that needs a DP table over every pair of prefixes, not a single one-pass claim where only one string is ever allowed to skip. See Two-Sequence DP
- The two pointers start at opposite ends of one string and move toward each other comparing characters for equality, as in Valid Palindrome (LC 125), instead of one pointer claiming characters of a second, separate string. See Converging Pointers
Deep Dive
Why the earliest match is always safe
A subsequence keeps the order of the characters but may skip characters. So s is a subsequence of t when the characters of s can be found in t from left to right.
The variable i counts the characters of s that are already claimed. The next character to claim is s[i]. The loop reads t once with j. When t[j] equals s[i], the code claims it and does i += 1.
Taking the first copy of s[i] in t never loses a later match. Every character of t after j is still unread, so it is still free for s[i + 1] and the rest of s. A later copy of the same character would leave fewer characters of t for the rest of s. So the earliest copy is at least as good as any other copy.
The code has no search and no table. This card is its own root: it is not a merge of two sequences. Only t is read in full, and only s is allowed to stay at the same position.
Tracing s = "abc" in t = "ahbgdcxy"
The board shows t in the top row and s in the bottom row. The tag i is the next character of s that is not claimed. The tag j is the character of t that is read now.
t[0] is a and matches s[0], so i becomes 1. Then t[1] is h, and s[1] is b. They differ, so i stays 1. This is the step that the trap changes: i moves only on a match.
t[2] is b and matches, so i becomes 2. t[3] is g and t[4] is d. Neither equals c, so i stays 2. t[5] is c and matches, so i becomes 3.
Now i equals len(s), which is 3. The last two characters of t, x and y, are read and skipped. The final check i == len(s) returns True. This is a longer input than the first example of Is Subsequence (LC 392), where s is abc and t is ahbgdc. The result is the same.
What the final check and the cost say
i can only grow by one at a time, and only on a match. So i == len(s) is true exactly when every character of s was claimed in order. When the loop ends with a smaller i, the characters from s[i] onward were not found after the previous match. For s = "axc" and t = "ahbgdc", the answer is False, because x does not appear in t.
An empty s returns True, because i starts at 0 and len(s) is 0. An empty t with a non-empty s returns False.
The loop reads each character of t once and never moves backward. The time is O(len(t)). The code stores only i and j, so the extra space is O(1).
If the problem gives many strings s for the same t, this loop would read t again for every s. A common change is to store, for each character, the sorted list of its positions in t, and to find the next position with a binary search. That is a different card; this one needs only one string s.
60-Second Pitch
"I keep a counter i of the characters of s that I have claimed. I read t once with j. When t[j] equals s[i], I claim it with i += 1. Otherwise I skip t[j] and i stays. Taking the earliest match is safe. All later characters of t stay free for the rest of s, so no earlier choice can cost a later match. At the end, s is a subsequence of t exactly when i == len(s). The time is O(len(t)) and the extra space is O(1)."
Common Questions
- Why is taking the earliest match correct?
After the earliest copy of
s[i]int, every later character oftis still unread. Any other copy ofs[i]is at the same position or later, so it leaves the same or fewer characters for the rest ofs. No answer that uses a later copy can succeed when the earliest copy fails.- What is the time and space complexity?
The time is O(len(t)), because
jvisits each character oftonce. The extra space isO(1), because the code stores onlyiandj.- Why does this not need dynamic programming?
Only
tmay skip characters in an unlimited way, and the choice at each match is forced to be the earliest one. So there is one possible path, and a single pass finds it. Longest Common Subsequence (LC 1143) lets both strings skip and asks for the best length, so it needs a table.- What changes when there are many strings
sfor onet? Reading
tagain for eachscosts O(len(t)) per query. Store, for each character, the sorted positions int. For each character ofs, binary search the first position after the previous match. This is the follow-up of Is Subsequence (LC 392).- What does the code return for an empty
s? It returns
True.istarts at 0,len(s)is 0, and the checki == len(s)is true before any character oftis read.
SWE Reasoning
Spot it
- The problem asks whether every character of one string appears, in the same order, inside a second string, and the second string may be read with skips.
- The answer is a yes or no, not a count or a best alignment.
- One pass over the longer string with
O(1)extra space is expected.
The invariant, step by step
i = 0. No character ofsis claimed yet.- The loop reads
t[j]forjfrom 0 tolen(t) - 1. - After
t[0..j]is read,s[0..i-1]already appears, in order, insidet[0..j]. - If
i < len(s)ands[i] == t[j], the code claimss[i]withi += 1. The earliest copy leaves every later character oftfree, so the invariant holds. - If the characters differ,
istays the same andt[j]is skipped. The invariant holds. return i == len(s)is true exactly when all ofswas claimed.
Traps
i += 1on every character oft.ithen counts the characters oft, and the final check passes for pairs that are not a subsequence match.i == len(s) - 1.ireacheslen(s)after the last claim, so the check rejects a correct full match.- Returning
Truewhen the loop reaches the last index oft. Reading all oftdoes not mean all ofswas claimed. ch in tfor each character ofs. This ignores the order. Fors = "ba"andt = "ab", both characters exist int, but not in the order ofs.- Reading
s[i]wheni == len(s). Checki < len(s)first, or the index is out of range.