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•Dynamic Programming

Builds on Two-Sequence DP, which you haven’t finished yet.

P2HardBuilt on Grid DP~3 min speedrun

Distinct Subsequences Counting DP

Built on
Two-Sequence DPIt counts the ways one string can be picked out of another, so each cell adds counts instead of taking a best value.
The problem

Distinct Subsequences (LC 115)(opens leetcode.com)

You get two strings, s and t. Choose some positions of s and read their letters from left to right, skipping the others: the result is a subsequence of s.

Count how many different choices of positions spell exactly t. Two choices are different when at least one position differs, even though both spell t. Upper-case and lower-case letters are different letters. The answer is never larger than 2^31 - 1.

Example 1
Input:s = "rabbbit", t = "rabbit"
Output:3
s has three bs in a row and t needs two of them; every other letter of t has exactly one place it can come from. Two of three bs can be chosen in 3 ways.
s
r0a1b2b3b4i5t6
t
r0a1b2b3i4t5

Distinct Subsequences

Spot it when: You must count how many different ways the letters of one string t can be picked out of another string s, in order with gaps allowed, or how many distinct subsequences a string has: a count of subsequence matches, not one longest or cheapest alignment.
Distinct Subsequences
Python
1def num_distinct(s: str, t: str) -> int:
2 """Distinct Subsequences: count the ways t can be picked out of s, one character of s at a time.
3 Time: O(M * N) | Space: O(N)
4 """
5 n = len(t)
6 ways = [1] + [0] * n # ways[j]: ways to spell t[:j] from the part of s read so far; t[:0] once, by picking nothing
7 
8 for ch in s: # Each character of s is either used or skipped
9 for j in range(n, 0, -1): # Backwards: ways[j - 1] still counts only the characters before ch
10 if t[j - 1] == ch:
11 ways[j] += ways[j - 1] # Old ways[j] skip ch; the ways[j - 1] ways finish t[:j] with ch
12 
13 return ways[n]
Invariant: After each character of s is read, ways[j] is the number of ways to spell t[:j] with the characters read so far, and ways[0] = 1.

Read s one character ch at a time and keep ways[j], the number of ways to spell the first j letters of t with what you have read so far. A new ch changes nothing unless t[j - 1] == ch; then every way to spell t[:j - 1] can finish with ch, so ways[j] += ways[j - 1], while the old ways[j] stays counted as the ways that skip ch. Nothing else is needed, because each count already covers every way to use the characters read so far; ways[0] = 1 because the empty prefix is spelled once, by picking nothing. The trap is the direction of j: run it from n down to 1, so ways[j - 1] is read before this same ch changes it. Going up, one letter of s fills two places of t, and "rabbbit", "rabbit" returns 6 instead of 3.

See it run

Distinct Subsequences, step by step

ch - · ways [1, 0, 0, 0, 0, 0, 0]

ch r · ways [1, 1, 0, 0, 0, 0, 0]

ch a · ways [1, 1, 1, 0, 0, 0, 0]

ch b · ways [1, 1, 1, 0, 0, 0, 0]

ch b · ways [1, 1, 1, 1, 0, 0, 0]

ch b · ways [1, 1, 1, 1, 1, 0, 0]

ch b · ways [1, 1, 1, 2, 1, 0, 0]

ch b · ways [1, 1, 1, 2, 3, 0, 0]

ch b · ways [1, 1, 1, 3, 3, 0, 0]

ch i · ways [1, 1, 1, 3, 3, 3, 0]

ch t · ways [1, 1, 1, 3, 3, 3, 3]

ways[n] 3

Row k is the list ways after the k-th letter of s. Column j counts the ways to spell t[:j].

  • new the cell being written (also the starting cell ways[0])
  • finish the old ways[j - 1], which finishes t[:j] with this letter
  • skip the old ways[j], which skips this letter
  • answer the final answer

Start: s = "rabbbit" and t = "rabbit". ways = [1] + [0] * 6. ways[0] = 1 because the empty prefix of t is spelled once, by picking nothing.

Read s[0] = "r", and run j from 6 down to 1. j = 1: t[0] = "r" matches, so ways[1] += ways[0], which is 0 + 1 = 1.

Read s[1] = "a", and run j from 6 down to 1. j = 2: t[1] = "a" matches, so ways[2] += ways[1], which is 0 + 1 = 1.

Read s[2] = "b", and run j from 6 down to 1. j = 4: t[3] = "b" matches, so ways[4] += ways[3], which is 0 + 0 = 0.

j = 3: t[2] = "b" matches, so ways[3] += ways[2], which is 0 + 1 = 1.

Read s[3] = "b", and run j from 6 down to 1. j = 4: t[3] = "b" matches, so ways[4] += ways[3], which is 0 + 1 = 1.

j = 3: t[2] = "b" matches, so ways[3] += ways[2], which is 1 + 1 = 2.

Read s[4] = "b", and run j from 6 down to 1. j = 4: t[3] = "b" matches, so ways[4] += ways[3], which is 1 + 2 = 3.

j = 3: t[2] = "b" matches, so ways[3] += ways[2], which is 2 + 1 = 3.

Read s[5] = "i", and run j from 6 down to 1. j = 5: t[4] = "i" matches, so ways[5] += ways[4], which is 0 + 3 = 3.

Read s[6] = "t", and run j from 6 down to 1. j = 6: t[5] = "t" matches, so ways[6] += ways[5], which is 0 + 3 = 3.

Every letter of s is read. ways[6] = 3, the number of ways to pick "rabbit" out of "rabbbit".

start
empty1new
r0
ra0
rab0
rabb0
rabbi0
rabbit0
r
a
b
b
b
i
t
1 / 12
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Running j upward instead of from n down to 1

    Mistake
    for j in range(1, n + 1):
    Fix
    for j in range(n, 0, -1):

    What happens: ways[j] += ways[j - 1] must read ways[j - 1] from before the current ch. Going upward, ways[j - 1] was already changed by this same ch. So one letter of s fills two places of t.

    Example: s = "rabbbit", t = "rabbit"
    Mistake: returns 6Fix: returns 3
  2. 2

    Gotcha 2: Writing ways[j] = ways[j - 1] instead of +=

    Mistake
    ways[j] = ways[j - 1]
    Fix
    ways[j] += ways[j - 1]

    What happens: The old ways[j] counts the ways that skip ch. Those ways are still valid. = throws them away and keeps only the ways that use ch.

    Example: s = "ccc", t = "c"
    Mistake: returns 1Fix: returns 3
  3. 3

    Gotcha 3: Starting with ways[0] = 0

    Mistake
    ways = [0] + [0] * n
    Fix
    ways = [1] + [0] * n

    What happens: The empty prefix t[:0] is spelled in exactly one way, by picking nothing. Every match adds ways[j - 1], and the first match reads ways[0]. With ways[0] = 0, every count stays 0.

    Example: s = "ccc", t = "c"
    Mistake: returns 0Fix: returns 3
  4. 4

    Gotcha 4: Comparing t[j] instead of t[j - 1]

    Mistake
    if t[j] == ch:
    Fix
    if t[j - 1] == ch:

    What happens: ways[j] is the count for the prefix t[:j], whose last letter is t[j - 1]. t[j] is one letter too far. At j = n, it reads past the end of t.

    Example: s = "ccc", t = "c"
    Mistake: raises IndexErrorFix: returns 3
Not this one if…
  • The question asks for the length of the best match between two strings, as in Longest Common Subsequence (LC 1143), not for a count of the ways one string can be picked out of the other. See Two-Sequence DP
  • There is no second string t to match against, as in Distinct Subsequences II (LC 940): the question counts every distinct subsequence of one string, by a running count per last letter, not the ways to spell a fixed target. See Last-Char Counting DP

Deep Dive

The observation: count by the last letter of the prefix

The problem asks how many ways the string t can be picked out of s. A way is a set of positions in s, in increasing order, whose letters spell t. Two ways are different when their sets of positions are different, even if the letters are the same.

The observation is to read s one letter at a time and to count the ways for every prefix of t at once. ways[j] is the number of ways to spell t[:j] from the part of s read so far. The answer is ways[n] after the last letter.

Example: for s = "rabbbit" and t = "rabbit", the three b letters of s give three ways to pick the two b letters of t. That is why the answer is 3.

Why ways[j] += ways[j - 1] is correct

When a new letter ch is read, every way to spell t[:j] either skips ch or uses it as its last letter.

The ways that skip ch are the old ways[j]. They stay valid, so += keeps them.

The ways that use ch as the last letter exist only when t[j - 1] == ch. Each one is a way to spell t[:j - 1] from the earlier letters, followed by ch. Their number is ways[j - 1], read from before this ch.

The two groups share no way, because one uses the position of ch and the other does not. So the new ways[j] is the sum of the two groups.

ways[0] = 1 because the empty prefix is spelled one time, by picking nothing. Without it, every sum would start from 0.

What this card adds to Two-Sequence DP

Two-Sequence DP already teaches that a state is a pair of prefixes, one of s and one of t, and that a row depends only on the row above it. This card keeps that state and that row-by-row order.

The new part is the question. Two-Sequence DP takes the best value of neighbouring cells. This card adds the counts of two cells, because it counts ways.

In a table, the update is dp[i][j] = dp[i - 1][j] + dp[i - 1][j - 1] when the letters match, and dp[i][j] = dp[i - 1][j] when they differ. The first term counts the ways that skip the letter. The second term counts the ways that use it.

One list ways holds the row. It is updated in place, and that is why the direction of j matters.

Why j runs from n down to 1, and the cost

The update for j reads ways[j - 1] from the row above. If j runs upward, ways[j - 1] was already changed by the same ch. One letter of s would then fill two places of t.

Running j downward changes ways[j] before ways[j - 1]. So ways[j - 1] still holds the value from before ch.

Example: s = "rabbbit", t = "rabbit" returns 3 with the downward loop and 6 with the upward loop. For s = "aaaa" and t = "aa", the correct answer is 6 and the upward loop returns 10.

There are M letters in s and N letters in t. Each pair is visited once with constant work, so the time is O(M * N). The list ways has N + 1 entries, so the space is O(N).

The count can grow quickly. LeetCode guarantees that the answer fits in a 32-bit integer. Python integers do not overflow, but in a language with fixed-size integers the intermediate values need the same guarantee.

60-Second Pitch

"The brute force chooses positions in s that spell t. That grows exponentially, so I count instead. I read s one letter at a time and keep ways[j], the number of ways to spell the first j letters of t so far, with ways[0] = 1. When the letter ch equals t[j - 1], every way to spell t[:j - 1] can end with ch. So I add ways[j - 1] to ways[j]. The old ways[j] stays, because it counts the ways that skip ch. I loop j from n down to 1, so ways[j - 1] is read before this letter changes it. That gives O(M * N) time and O(N) space. For s = "babgbag" and t = "bag", the answer is 5."

Common Questions

Why does the inner loop run backwards?

The update for j needs ways[j - 1] from before the current letter. Going backwards changes ways[j] before ways[j - 1], so the older value is still there. Going forwards lets one letter of s fill two places of t.

How do you write this as a two-dimensional table?

Let dp[i][j] be the ways to spell t[:j] from s[:i]. Set dp[i][0] = 1. Then dp[i][j] = dp[i - 1][j], plus dp[i - 1][j - 1] when s[i - 1] == t[j - 1]. The answer is dp[M][N]. The card's list keeps only one row, which is why the space is O(N) and not O(M * N).

What is the time and space complexity, and can the space be smaller?

O(M * N) time and O(N) space, where M = len(s) and N = len(t). If N is larger than M, the answer is 0, so the function can return at once. After that check, N <= M, so the list over t is the shorter one.

What does the function return when t is longer than s, or when t is empty?

When t is longer than s, it returns 0, because t cannot be spelled. For s = "abc" and t = "abcd" the result is 0. When t is empty, it returns 1, because picking nothing spells the empty string.

How is this different from Longest Common Subsequence (LC 1143) and Distinct Subsequences II (LC 940)?

Longest Common Subsequence (LC 1143) uses the same pair of prefixes but takes a maximum length, so each cell takes the best of its neighbours. This card keeps that state and changes only the combining step. Distinct Subsequences II (LC 940) has one string and counts different texts, so it keeps a count per last letter. This problem counts position sets that spell a fixed t, so each cell adds counts.

SWE Reasoning

Spot it

  • Two strings are given, and the question asks how many ways one is picked out of the other, not how long or how cheap the best match is.
  • Order matters and gaps are allowed, so the picked letters form a subsequence of s.
  • Different positions count as different ways, even when they spell the same text.
  • The brute force chooses positions in s and grows exponentially. The state (letters of s read, letters of t matched) has only M * N values.
  • The answer is a sum over two disjoint cases: the current letter is used, or it is skipped.

The invariant, step by step

  1. ways = [1] + [0] * n is set before any letter is read. ways[0] = 1, and every other ways[j] is 0 because no letter of s has been read yet.
  2. The loop reads one letter ch of s. At this moment ways[j] is correct for all j for the letters read before ch.
  3. The inner loop visits j from n down to 1. For a j where t[j - 1] == ch, it adds ways[j - 1] to ways[j].
  4. The old ways[j] is kept. These are the ways that skip ch. The added value ways[j - 1] is read before ch changes it, because the loop goes downward and changes ways[j - 1] only after it has finished with ways[j].
  5. Each j where t[j - 1] != ch is left unchanged. No new way ends with ch there.
  6. After the last letter, ways[j] counts the ways to spell t[:j] from the whole s. ways[n] is the answer.
  7. Trace for s = "babgbag" and t = "bag": ways after each letter is [1,1,0,0], [1,1,1,0], [1,2,1,0], [1,2,1,1], [1,3,1,1], [1,3,4,1], [1,3,4,5]. The answer is 5.

Traps

  • j runs upward. ways[j - 1] was already changed by the same ch, so one letter of s is used for two letters of t. num_distinct("rabbbit", "rabbit") returns 6 instead of 3.
  • = instead of +=. The old ways[j] holds the ways that skip ch. Assignment throws them away. num_distinct("ccc", "c") returns 1 instead of 3.
  • ways[0] = 0. The first match reads ways[0]. With 0, every count stays 0. The same call returns 0.
  • t[j] instead of t[j - 1]. ways[j] ends with the letter t[j - 1]. Using t[j] is off by one and raises IndexError at j = n.
  • Treating equal text as one way. The problem counts position sets. Two different sets of positions that spell t are two ways. Removing duplicates gives a smaller, wrong answer.
  • Using this card for Distinct Subsequences II (LC 940). That problem has no target string t. It counts different texts, so it needs a count per last letter and a replace step, not a prefix of t.

Practice

Canonical LeetCode:Distinct Subsequences (LC 115)Number of Ways to Form a Target String Given a Dictionary (LC 1639)
Theory Context•Dynamic Programming

Builds on Two-Sequence DP, which you haven’t finished yet.

P2HardBuilt on Grid DP~3 min speedrun

Distinct Subsequences Counting DP

Built on
Two-Sequence DPIt counts the ways one string can be picked out of another, so each cell adds counts instead of taking a best value.
The problem

Distinct Subsequences (LC 115)(opens leetcode.com)

You get two strings, s and t. Choose some positions of s and read their letters from left to right, skipping the others: the result is a subsequence of s.

Count how many different choices of positions spell exactly t. Two choices are different when at least one position differs, even though both spell t. Upper-case and lower-case letters are different letters. The answer is never larger than 2^31 - 1.

Example 1
Input:s = "rabbbit", t = "rabbit"
Output:3
s has three bs in a row and t needs two of them; every other letter of t has exactly one place it can come from. Two of three bs can be chosen in 3 ways.
s
r0a1b2b3b4i5t6
t
r0a1b2b3i4t5

Distinct Subsequences

Spot it when: You must count how many different ways the letters of one string t can be picked out of another string s, in order with gaps allowed, or how many distinct subsequences a string has: a count of subsequence matches, not one longest or cheapest alignment.
Distinct Subsequences
Python
1def num_distinct(s: str, t: str) -> int:
2 """Distinct Subsequences: count the ways t can be picked out of s, one character of s at a time.
3 Time: O(M * N) | Space: O(N)
4 """
5 n = len(t)
6 ways = [1] + [0] * n # ways[j]: ways to spell t[:j] from the part of s read so far; t[:0] once, by picking nothing
7 
8 for ch in s: # Each character of s is either used or skipped
9 for j in range(n, 0, -1): # Backwards: ways[j - 1] still counts only the characters before ch
10 if t[j - 1] == ch:
11 ways[j] += ways[j - 1] # Old ways[j] skip ch; the ways[j - 1] ways finish t[:j] with ch
12 
13 return ways[n]
Invariant: After each character of s is read, ways[j] is the number of ways to spell t[:j] with the characters read so far, and ways[0] = 1.

Read s one character ch at a time and keep ways[j], the number of ways to spell the first j letters of t with what you have read so far. A new ch changes nothing unless t[j - 1] == ch; then every way to spell t[:j - 1] can finish with ch, so ways[j] += ways[j - 1], while the old ways[j] stays counted as the ways that skip ch. Nothing else is needed, because each count already covers every way to use the characters read so far; ways[0] = 1 because the empty prefix is spelled once, by picking nothing. The trap is the direction of j: run it from n down to 1, so ways[j - 1] is read before this same ch changes it. Going up, one letter of s fills two places of t, and "rabbbit", "rabbit" returns 6 instead of 3.

See it run

Distinct Subsequences, step by step

ch - · ways [1, 0, 0, 0, 0, 0, 0]

ch r · ways [1, 1, 0, 0, 0, 0, 0]

ch a · ways [1, 1, 1, 0, 0, 0, 0]

ch b · ways [1, 1, 1, 0, 0, 0, 0]

ch b · ways [1, 1, 1, 1, 0, 0, 0]

ch b · ways [1, 1, 1, 1, 1, 0, 0]

ch b · ways [1, 1, 1, 2, 1, 0, 0]

ch b · ways [1, 1, 1, 2, 3, 0, 0]

ch b · ways [1, 1, 1, 3, 3, 0, 0]

ch i · ways [1, 1, 1, 3, 3, 3, 0]

ch t · ways [1, 1, 1, 3, 3, 3, 3]

ways[n] 3

Row k is the list ways after the k-th letter of s. Column j counts the ways to spell t[:j].

  • new the cell being written (also the starting cell ways[0])
  • finish the old ways[j - 1], which finishes t[:j] with this letter
  • skip the old ways[j], which skips this letter
  • answer the final answer

Start: s = "rabbbit" and t = "rabbit". ways = [1] + [0] * 6. ways[0] = 1 because the empty prefix of t is spelled once, by picking nothing.

Read s[0] = "r", and run j from 6 down to 1. j = 1: t[0] = "r" matches, so ways[1] += ways[0], which is 0 + 1 = 1.

Read s[1] = "a", and run j from 6 down to 1. j = 2: t[1] = "a" matches, so ways[2] += ways[1], which is 0 + 1 = 1.

Read s[2] = "b", and run j from 6 down to 1. j = 4: t[3] = "b" matches, so ways[4] += ways[3], which is 0 + 0 = 0.

j = 3: t[2] = "b" matches, so ways[3] += ways[2], which is 0 + 1 = 1.

Read s[3] = "b", and run j from 6 down to 1. j = 4: t[3] = "b" matches, so ways[4] += ways[3], which is 0 + 1 = 1.

j = 3: t[2] = "b" matches, so ways[3] += ways[2], which is 1 + 1 = 2.

Read s[4] = "b", and run j from 6 down to 1. j = 4: t[3] = "b" matches, so ways[4] += ways[3], which is 1 + 2 = 3.

j = 3: t[2] = "b" matches, so ways[3] += ways[2], which is 2 + 1 = 3.

Read s[5] = "i", and run j from 6 down to 1. j = 5: t[4] = "i" matches, so ways[5] += ways[4], which is 0 + 3 = 3.

Read s[6] = "t", and run j from 6 down to 1. j = 6: t[5] = "t" matches, so ways[6] += ways[5], which is 0 + 3 = 3.

Every letter of s is read. ways[6] = 3, the number of ways to pick "rabbit" out of "rabbbit".

start
empty1new
r0
ra0
rab0
rabb0
rabbi0
rabbit0
r
a
b
b
b
i
t
1 / 12
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Running j upward instead of from n down to 1

    Mistake
    for j in range(1, n + 1):
    Fix
    for j in range(n, 0, -1):

    What happens: ways[j] += ways[j - 1] must read ways[j - 1] from before the current ch. Going upward, ways[j - 1] was already changed by this same ch. So one letter of s fills two places of t.

    Example: s = "rabbbit", t = "rabbit"
    Mistake: returns 6Fix: returns 3
  2. 2

    Gotcha 2: Writing ways[j] = ways[j - 1] instead of +=

    Mistake
    ways[j] = ways[j - 1]
    Fix
    ways[j] += ways[j - 1]

    What happens: The old ways[j] counts the ways that skip ch. Those ways are still valid. = throws them away and keeps only the ways that use ch.

    Example: s = "ccc", t = "c"
    Mistake: returns 1Fix: returns 3
  3. 3

    Gotcha 3: Starting with ways[0] = 0

    Mistake
    ways = [0] + [0] * n
    Fix
    ways = [1] + [0] * n

    What happens: The empty prefix t[:0] is spelled in exactly one way, by picking nothing. Every match adds ways[j - 1], and the first match reads ways[0]. With ways[0] = 0, every count stays 0.

    Example: s = "ccc", t = "c"
    Mistake: returns 0Fix: returns 3
  4. 4

    Gotcha 4: Comparing t[j] instead of t[j - 1]

    Mistake
    if t[j] == ch:
    Fix
    if t[j - 1] == ch:

    What happens: ways[j] is the count for the prefix t[:j], whose last letter is t[j - 1]. t[j] is one letter too far. At j = n, it reads past the end of t.

    Example: s = "ccc", t = "c"
    Mistake: raises IndexErrorFix: returns 3
Not this one if…
  • The question asks for the length of the best match between two strings, as in Longest Common Subsequence (LC 1143), not for a count of the ways one string can be picked out of the other. See Two-Sequence DP
  • There is no second string t to match against, as in Distinct Subsequences II (LC 940): the question counts every distinct subsequence of one string, by a running count per last letter, not the ways to spell a fixed target. See Last-Char Counting DP

Deep Dive

The observation: count by the last letter of the prefix

The problem asks how many ways the string t can be picked out of s. A way is a set of positions in s, in increasing order, whose letters spell t. Two ways are different when their sets of positions are different, even if the letters are the same.

The observation is to read s one letter at a time and to count the ways for every prefix of t at once. ways[j] is the number of ways to spell t[:j] from the part of s read so far. The answer is ways[n] after the last letter.

Example: for s = "rabbbit" and t = "rabbit", the three b letters of s give three ways to pick the two b letters of t. That is why the answer is 3.

Why ways[j] += ways[j - 1] is correct

When a new letter ch is read, every way to spell t[:j] either skips ch or uses it as its last letter.

The ways that skip ch are the old ways[j]. They stay valid, so += keeps them.

The ways that use ch as the last letter exist only when t[j - 1] == ch. Each one is a way to spell t[:j - 1] from the earlier letters, followed by ch. Their number is ways[j - 1], read from before this ch.

The two groups share no way, because one uses the position of ch and the other does not. So the new ways[j] is the sum of the two groups.

ways[0] = 1 because the empty prefix is spelled one time, by picking nothing. Without it, every sum would start from 0.

What this card adds to Two-Sequence DP

Two-Sequence DP already teaches that a state is a pair of prefixes, one of s and one of t, and that a row depends only on the row above it. This card keeps that state and that row-by-row order.

The new part is the question. Two-Sequence DP takes the best value of neighbouring cells. This card adds the counts of two cells, because it counts ways.

In a table, the update is dp[i][j] = dp[i - 1][j] + dp[i - 1][j - 1] when the letters match, and dp[i][j] = dp[i - 1][j] when they differ. The first term counts the ways that skip the letter. The second term counts the ways that use it.

One list ways holds the row. It is updated in place, and that is why the direction of j matters.

Why j runs from n down to 1, and the cost

The update for j reads ways[j - 1] from the row above. If j runs upward, ways[j - 1] was already changed by the same ch. One letter of s would then fill two places of t.

Running j downward changes ways[j] before ways[j - 1]. So ways[j - 1] still holds the value from before ch.

Example: s = "rabbbit", t = "rabbit" returns 3 with the downward loop and 6 with the upward loop. For s = "aaaa" and t = "aa", the correct answer is 6 and the upward loop returns 10.

There are M letters in s and N letters in t. Each pair is visited once with constant work, so the time is O(M * N). The list ways has N + 1 entries, so the space is O(N).

The count can grow quickly. LeetCode guarantees that the answer fits in a 32-bit integer. Python integers do not overflow, but in a language with fixed-size integers the intermediate values need the same guarantee.

60-Second Pitch

"The brute force chooses positions in s that spell t. That grows exponentially, so I count instead. I read s one letter at a time and keep ways[j], the number of ways to spell the first j letters of t so far, with ways[0] = 1. When the letter ch equals t[j - 1], every way to spell t[:j - 1] can end with ch. So I add ways[j - 1] to ways[j]. The old ways[j] stays, because it counts the ways that skip ch. I loop j from n down to 1, so ways[j - 1] is read before this letter changes it. That gives O(M * N) time and O(N) space. For s = "babgbag" and t = "bag", the answer is 5."

Common Questions

Why does the inner loop run backwards?

The update for j needs ways[j - 1] from before the current letter. Going backwards changes ways[j] before ways[j - 1], so the older value is still there. Going forwards lets one letter of s fill two places of t.

How do you write this as a two-dimensional table?

Let dp[i][j] be the ways to spell t[:j] from s[:i]. Set dp[i][0] = 1. Then dp[i][j] = dp[i - 1][j], plus dp[i - 1][j - 1] when s[i - 1] == t[j - 1]. The answer is dp[M][N]. The card's list keeps only one row, which is why the space is O(N) and not O(M * N).

What is the time and space complexity, and can the space be smaller?

O(M * N) time and O(N) space, where M = len(s) and N = len(t). If N is larger than M, the answer is 0, so the function can return at once. After that check, N <= M, so the list over t is the shorter one.

What does the function return when t is longer than s, or when t is empty?

When t is longer than s, it returns 0, because t cannot be spelled. For s = "abc" and t = "abcd" the result is 0. When t is empty, it returns 1, because picking nothing spells the empty string.

How is this different from Longest Common Subsequence (LC 1143) and Distinct Subsequences II (LC 940)?

Longest Common Subsequence (LC 1143) uses the same pair of prefixes but takes a maximum length, so each cell takes the best of its neighbours. This card keeps that state and changes only the combining step. Distinct Subsequences II (LC 940) has one string and counts different texts, so it keeps a count per last letter. This problem counts position sets that spell a fixed t, so each cell adds counts.

SWE Reasoning

Spot it

  • Two strings are given, and the question asks how many ways one is picked out of the other, not how long or how cheap the best match is.
  • Order matters and gaps are allowed, so the picked letters form a subsequence of s.
  • Different positions count as different ways, even when they spell the same text.
  • The brute force chooses positions in s and grows exponentially. The state (letters of s read, letters of t matched) has only M * N values.
  • The answer is a sum over two disjoint cases: the current letter is used, or it is skipped.

The invariant, step by step

  1. ways = [1] + [0] * n is set before any letter is read. ways[0] = 1, and every other ways[j] is 0 because no letter of s has been read yet.
  2. The loop reads one letter ch of s. At this moment ways[j] is correct for all j for the letters read before ch.
  3. The inner loop visits j from n down to 1. For a j where t[j - 1] == ch, it adds ways[j - 1] to ways[j].
  4. The old ways[j] is kept. These are the ways that skip ch. The added value ways[j - 1] is read before ch changes it, because the loop goes downward and changes ways[j - 1] only after it has finished with ways[j].
  5. Each j where t[j - 1] != ch is left unchanged. No new way ends with ch there.
  6. After the last letter, ways[j] counts the ways to spell t[:j] from the whole s. ways[n] is the answer.
  7. Trace for s = "babgbag" and t = "bag": ways after each letter is [1,1,0,0], [1,1,1,0], [1,2,1,0], [1,2,1,1], [1,3,1,1], [1,3,4,1], [1,3,4,5]. The answer is 5.

Traps

  • j runs upward. ways[j - 1] was already changed by the same ch, so one letter of s is used for two letters of t. num_distinct("rabbbit", "rabbit") returns 6 instead of 3.
  • = instead of +=. The old ways[j] holds the ways that skip ch. Assignment throws them away. num_distinct("ccc", "c") returns 1 instead of 3.
  • ways[0] = 0. The first match reads ways[0]. With 0, every count stays 0. The same call returns 0.
  • t[j] instead of t[j - 1]. ways[j] ends with the letter t[j - 1]. Using t[j] is off by one and raises IndexError at j = n.
  • Treating equal text as one way. The problem counts position sets. Two different sets of positions that spell t are two ways. Removing duplicates gives a smaller, wrong answer.
  • Using this card for Distinct Subsequences II (LC 940). That problem has no target string t. It counts different texts, so it needs a count per last letter and a replace step, not a prefix of t.

Practice

Canonical LeetCode:Distinct Subsequences (LC 115)Number of Ways to Form a Target String Given a Dictionary (LC 1639)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•