Hi👋SpeedAlgo • Deliberate Practice & Cognitive Ergonomics for Software Engineers

An interactive algorithm mastery and technical interview preparation platform published by Hi👋WebEnterprise. Built for senior and staff software engineers preparing for rigorous coding screens at top tech companies (FAANG/MAMAA).

13 Core Algorithmic Patterns & 225 Practice Problems

  • 1. Two Pointers (16 Paradigms, 43 Problems): Opposite ends, sorted pair sums, palindrome verification, trapping rain water, 3Sum, container with most water — includes the Sliding Window and Fast & Slow Pointers paradigms (contiguous subarray invariants, longest substrings, Floyd's cycle detection, linked list middle).
  • 2. Binary Search (9 Paradigms, 14 Problems): Monotonic predicate partitioning (feasible(x)), boundary search, rotated sorted arrays, square roots, minimum ship capacity, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 11 Problems): Counting set bits, XOR cancellation, bit reversal, bitmask pairing, maximum XOR with a bitwise trie.
  • 4. Math & Geometry (8 Paradigms, 14 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (15 Paradigms, 24 Problems): Path sums, lowest common ancestor, tree diameter, validating BSTs, tree DP (House Robber III, Binary Tree Cameras).
  • 6. Tree/Graph Breadth-First Search (5 Paradigms, 12 Problems): Level-order traversals, shortest path in unweighted graphs, zig-zag traversals, rotting oranges, word ladders.
  • 7. Graphs (12 Paradigms, 22 Problems): Topological sort, cycle detection, Dijkstra's algorithm, bipartite graph validation, network delay.
  • 8. Backtracking (6 Paradigms, 14 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens, Sudoku solver.
  • 9. Dynamic Programming (13 Paradigms, 28 Problems): Memoization vs tabulation, 0/1 knapsack, unbounded knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (9 Paradigms, 11 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (7 Paradigms, 17 Problems): Trie (prefix tree), Union-Find (Disjoint Set Union), LRU Cache, LFU Cache, range-sum queries with updates.
  • 12. Stack (5 Paradigms, 9 Problems): Next greater element, largest rectangle in histogram, remove k digits, decode string, min stack.
  • 13. Intervals & Greedy (2 Paradigms, 6 Problems): Merge intervals, interval scheduling, meeting rooms by sweep line, jump game.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Read the pattern's mental model, loop invariant and Big-O proof, compare the pattern's template with the problem's solution side by side, and use the step-by-step debugger. The solution can also be shown in C#, Java, TypeScript, C++, Go or Rust for almost every problem.
  2. Stage 2 (Active Recall): Rebuild the solution from memory in a code editor, with an invariant checklist and optional progressive hints, and check it against a judged test suite.
  3. Stage 3 (AI Interview Coach, coming soon): A mock-interview stage still in development; for now, students go straight from Stage 2 to Stage 4.
  4. Stage 4 (Solve on Your Own): Solve a new problem on your own against a 15-minute timer; your code is graded against a test suite. Python and TypeScript run in your browser and C# runs on our server (design problems grade in Python and TypeScript).

Equipped with a spaced-repetition review hub on a fixed 7-step schedule (1 day up to 90 days), a two-pane Studio workspace, and study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Coins: New accounts get 40 free coins once, the daily claim adds +20 coins once a day, and inviting a friend earns you +25 coins when they complete their first practice step. These are the only ways to earn coins.
  • AI Coach: Each AI coach question costs 1-5 coins, priced by the server according to its size.
  • BYOK (Bring Your Own Key): AI coaching is free with your own Google Gemini, OpenAI or OpenRouter API key (BYOK).
  • Extras: Coins also unlock extras for 24 hours: a Recall hint level (1 coin), a Solve solution reveal (2), a LeetCode problem card's solutions (1) and a pattern's extra canonical problems (1).
  • Payments & Refunds: Nothing on the site is sold for money: coins can only be earned, never bought. There are no subscriptions or charges, so there is nothing to refund.
  • Platform Operator: Hi👋WebEnterprise. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

Arena
238Items
Theory Context•Two Pointers & Sliding Window
P2EasyMental model~2 min speedrun

Subsequence Matching (Earliest-Match Two Pointers)

The problem

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.

Example 1
Input:s = "abc", t = "ahbgdc"
Output:true
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))
Spot it when: You must decide whether every character of one string, in order, can be found somewhere inside a second, usually longer string, and skipping characters of the second string is always allowed.
Greedy Subsequence Match
Python
1def 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)
Invariant: After 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 0 · j -

i 1 · j 0

i 1 · j 1

i 2 · j 2

i 2 · j 3

i 2 · j 4

i 3 · j 5

i 3 · j 6

i 3 · j 7

i 3 · j - · answer True

The top row is t and the bottom row is s. Cells are in string order, and each cell shows one character.

  • j the character of t that is read now
  • i the next character of s that is not claimed yet
  • claimed a character of s that is already matched, in order
  • match a 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.

t[0] is "a" and s[0] is "a". They are equal, so s[0] is claimed and i becomes 1.

Trap: t[1] is "h", but s[1] is "b", so i stays 1. Moving i here would claim "b" without a match.

t[2] is "b" and s[1] is "b". They are equal, so s[1] is claimed and i becomes 2.

t[3] is "g" and s[2] is "c". They differ, so t[3] is skipped and i stays 2.

t[4] is "d" and s[2] is "c". They differ, so t[4] is skipped and i stays 2.

t[5] is "c" and s[2] is "c". They are equal, so s[2] is claimed and i becomes 3.

i is 3, which equals len(s), so no character of s is left to claim. t[6] is skipped.

i is 3, which equals len(s), so no character of s is left to claim. t[7] is skipped.

Done: i is 3 and len(s) is 3, so i == len(s) is True.

t
0a
1h
2b
3g
4d
5c
6x
7y
s
ai
b
c
1 / 10
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Advancing i on every character of t, not only on a match (the card's trap)

    Mistake
    for j in range(len(t)):
        i += 1
    Fix
    for j in range(len(t)):
        if i < len(s) and s[i] == t[j]:
            i += 1

    What happens: i is supposed to count only the characters of s that were actually matched in t, in order. Moving i += 1 outside the if makes i count every character of t instead. i then reaches len(s) as soon as t is at least as long as s, no matter what characters either string holds, so the final check i == len(s) says True for pairs that are not a real subsequence match.

    Example: s = "abc", t = "ahbgdc"
    Mistake: returns FalseFix: returns True
  2. 2

    Gotcha 2: Writing i == len(s) - 1 instead of i == len(s)

    Mistake
    return i == len(s) - 1
    Fix
    return i == len(s)

    What happens: i reaches len(s) only once every character of s is claimed, since i starts at 0 and the last character claimed brings it to len(s), not len(s) - 1. Comparing against len(s) - 1 rejects a correct full match, one claim short of where the mistake expects it.

    Example: s = "abc", t = "ahbgdc"
    Mistake: returns FalseFix: returns True
  3. 3

    Gotcha 3: Returning True as soon as the scan of t finishes, instead of checking i

    Mistake
    if j == len(t) - 1:
        return True
    Fix
    return i == len(s)

    What happens: Reaching the last index of t only means the walk is over; it says nothing about whether every character of s was claimed. Returning True there confuses "finished scanning t" with "matched all of s", so the function reports a match for every non-empty t, even when most of s was never found.

    Example: s = "axc", t = "ahbgdc"
    Mistake: returns TrueFix: returns False
  4. 4

    Gotcha 4: Checking s[i] in t instead of walking t in order

    Mistake
    for ch in s:
        if ch in t:
            i += 1
    Fix
    for j in range(len(t)):
        if i < len(s) and s[i] == t[j]:
            i += 1

    What happens: ch in t only asks whether the character exists somewhere in t, with no memory of how far through t the previous match reached. Two characters of s can then both match the same early position of t, or match positions of t in the wrong order, and the mistake still counts both as claimed. The order that s must appear in is never checked.

    Example: s = "ba", t = "ab"
    Mistake: returns TrueFix: returns False
Not this one if…
  • 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] in t, every later character of t is still unread. Any other copy of s[i] is at the same position or later, so it leaves the same or fewer characters for the rest of s. 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 j visits each character of t once. The extra space is O(1), because the code stores only i and j.

Why does this not need dynamic programming?

Only t may 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 s for one t?

Reading t again for each s costs O(len(t)) per query. Store, for each character, the sorted positions in t. For each character of s, 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. i starts at 0, len(s) is 0, and the check i == len(s) is true before any character of t is 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

  1. i = 0. No character of s is claimed yet.
  2. The loop reads t[j] for j from 0 to len(t) - 1.
  3. After t[0..j] is read, s[0..i-1] already appears, in order, inside t[0..j].
  4. If i < len(s) and s[i] == t[j], the code claims s[i] with i += 1. The earliest copy leaves every later character of t free, so the invariant holds.
  5. If the characters differ, i stays the same and t[j] is skipped. The invariant holds.
  6. return i == len(s) is true exactly when all of s was claimed.

Traps

  • i += 1 on every character of t. i then counts the characters of t, and the final check passes for pairs that are not a subsequence match.
  • i == len(s) - 1. i reaches len(s) after the last claim, so the check rejects a correct full match.
  • Returning True when the loop reaches the last index of t. Reading all of t does not mean all of s was claimed.
  • ch in t for each character of s. This ignores the order. For s = "ba" and t = "ab", both characters exist in t, but not in the order of s.
  • Reading s[i] when i == len(s). Check i < len(s) first, or the index is out of range.

Practice

Canonical LeetCode:Is Subsequence (LC 392)Longest Word in Dictionary through Deleting (LC 524)Append Characters to String to Make Subsequence (LC 2486)Make String a Subsequence Using Cyclic Increments (LC 2825)
Theory Context•Two Pointers & Sliding Window
P2EasyMental model~2 min speedrun

Subsequence Matching (Earliest-Match Two Pointers)

The problem

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.

Example 1
Input:s = "abc", t = "ahbgdc"
Output:true
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))
Spot it when: You must decide whether every character of one string, in order, can be found somewhere inside a second, usually longer string, and skipping characters of the second string is always allowed.
Greedy Subsequence Match
Python
1def 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)
Invariant: After 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 0 · j -

i 1 · j 0

i 1 · j 1

i 2 · j 2

i 2 · j 3

i 2 · j 4

i 3 · j 5

i 3 · j 6

i 3 · j 7

i 3 · j - · answer True

The top row is t and the bottom row is s. Cells are in string order, and each cell shows one character.

  • j the character of t that is read now
  • i the next character of s that is not claimed yet
  • claimed a character of s that is already matched, in order
  • match a 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.

t[0] is "a" and s[0] is "a". They are equal, so s[0] is claimed and i becomes 1.

Trap: t[1] is "h", but s[1] is "b", so i stays 1. Moving i here would claim "b" without a match.

t[2] is "b" and s[1] is "b". They are equal, so s[1] is claimed and i becomes 2.

t[3] is "g" and s[2] is "c". They differ, so t[3] is skipped and i stays 2.

t[4] is "d" and s[2] is "c". They differ, so t[4] is skipped and i stays 2.

t[5] is "c" and s[2] is "c". They are equal, so s[2] is claimed and i becomes 3.

i is 3, which equals len(s), so no character of s is left to claim. t[6] is skipped.

i is 3, which equals len(s), so no character of s is left to claim. t[7] is skipped.

Done: i is 3 and len(s) is 3, so i == len(s) is True.

t
0a
1h
2b
3g
4d
5c
6x
7y
s
ai
b
c
1 / 10
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Advancing i on every character of t, not only on a match (the card's trap)

    Mistake
    for j in range(len(t)):
        i += 1
    Fix
    for j in range(len(t)):
        if i < len(s) and s[i] == t[j]:
            i += 1

    What happens: i is supposed to count only the characters of s that were actually matched in t, in order. Moving i += 1 outside the if makes i count every character of t instead. i then reaches len(s) as soon as t is at least as long as s, no matter what characters either string holds, so the final check i == len(s) says True for pairs that are not a real subsequence match.

    Example: s = "abc", t = "ahbgdc"
    Mistake: returns FalseFix: returns True
  2. 2

    Gotcha 2: Writing i == len(s) - 1 instead of i == len(s)

    Mistake
    return i == len(s) - 1
    Fix
    return i == len(s)

    What happens: i reaches len(s) only once every character of s is claimed, since i starts at 0 and the last character claimed brings it to len(s), not len(s) - 1. Comparing against len(s) - 1 rejects a correct full match, one claim short of where the mistake expects it.

    Example: s = "abc", t = "ahbgdc"
    Mistake: returns FalseFix: returns True
  3. 3

    Gotcha 3: Returning True as soon as the scan of t finishes, instead of checking i

    Mistake
    if j == len(t) - 1:
        return True
    Fix
    return i == len(s)

    What happens: Reaching the last index of t only means the walk is over; it says nothing about whether every character of s was claimed. Returning True there confuses "finished scanning t" with "matched all of s", so the function reports a match for every non-empty t, even when most of s was never found.

    Example: s = "axc", t = "ahbgdc"
    Mistake: returns TrueFix: returns False
  4. 4

    Gotcha 4: Checking s[i] in t instead of walking t in order

    Mistake
    for ch in s:
        if ch in t:
            i += 1
    Fix
    for j in range(len(t)):
        if i < len(s) and s[i] == t[j]:
            i += 1

    What happens: ch in t only asks whether the character exists somewhere in t, with no memory of how far through t the previous match reached. Two characters of s can then both match the same early position of t, or match positions of t in the wrong order, and the mistake still counts both as claimed. The order that s must appear in is never checked.

    Example: s = "ba", t = "ab"
    Mistake: returns TrueFix: returns False
Not this one if…
  • 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] in t, every later character of t is still unread. Any other copy of s[i] is at the same position or later, so it leaves the same or fewer characters for the rest of s. 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 j visits each character of t once. The extra space is O(1), because the code stores only i and j.

Why does this not need dynamic programming?

Only t may 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 s for one t?

Reading t again for each s costs O(len(t)) per query. Store, for each character, the sorted positions in t. For each character of s, 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. i starts at 0, len(s) is 0, and the check i == len(s) is true before any character of t is 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

  1. i = 0. No character of s is claimed yet.
  2. The loop reads t[j] for j from 0 to len(t) - 1.
  3. After t[0..j] is read, s[0..i-1] already appears, in order, inside t[0..j].
  4. If i < len(s) and s[i] == t[j], the code claims s[i] with i += 1. The earliest copy leaves every later character of t free, so the invariant holds.
  5. If the characters differ, i stays the same and t[j] is skipped. The invariant holds.
  6. return i == len(s) is true exactly when all of s was claimed.

Traps

  • i += 1 on every character of t. i then counts the characters of t, and the final check passes for pairs that are not a subsequence match.
  • i == len(s) - 1. i reaches len(s) after the last claim, so the check rejects a correct full match.
  • Returning True when the loop reaches the last index of t. Reading all of t does not mean all of s was claimed.
  • ch in t for each character of s. This ignores the order. For s = "ba" and t = "ab", both characters exist in t, but not in the order of s.
  • Reading s[i] when i == len(s). Check i < len(s) first, or the index is out of range.

Practice

Canonical LeetCode:Is Subsequence (LC 392)Longest Word in Dictionary through Deleting (LC 524)Append Characters to String to Make Subsequence (LC 2486)Make String a Subsequence Using Cyclic Increments (LC 2825)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•