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 State Machine DP, which you haven’t finished yet.

P3HardBuilt on State Machine DP~4 min speedrun

Last-Character Counting DP (Distinct Subsequences II)

Built on
State Machine DPThe mode is the last letter used, with up to 26 modes, and every count is updated together on each character.
The problem

Distinct Subsequences II (LC 940)(opens leetcode.com)

You get a string s of lowercase English letters. Deleting any number of its letters, possibly none, and keeping the rest in their original order gives a subsequence of s.

Count the different non-empty strings you can get this way. A string that several different choices of positions produce is counted once, not once per choice. The count can be very large, so return it modulo 109 + 7.

Example 1
Input:s = "abc"
Output:7
Every choice of letters gives a different string: "a", "b", "c", "ab", "ac", "bc" and "abc".

Last-Char Counting DP

s -> ends[26], one count per last letter -> replace on each new letter
Spot it when: You must count how many different non-empty subsequences a string has, with no second string to match against: count every subsequence of one string, not the ways to spell a fixed target.
Last-Char Counting DP
Python
1def distinctSubseqII(s: str) -> int:
2 """Counts the distinct non-empty subsequences of s, one running count per last letter.
3 Time: O(26 * N) = O(N) | Space: O(26) = O(1)
4 """
5 MOD = 10**9 + 7
6 ends = [0] * 26 # ends[c]: distinct non-empty subsequences built so far that end with letter c
7 for ch in s:
8 ends[ord(ch) - ord("a")] = (sum(ends) + 1) % MOD # Replace: the old ones ending in ch are rebuilt here
9 return sum(ends) % MOD
Invariant: After each letter of s is read, ends[c] is the number of different non-empty subsequences built so far that end in letter c, and every subsequence that already ended in c is already included inside that same count.

Keep one running count per last letter: ends[c] is how many different non-empty subsequences built so far end in c. A new letter ch can finish every one of those subsequences by appending ch to it, and it can also stand alone as the one-letter subsequence "ch", so ends[ch] = sum(ends) + 1 replaces the old ends[ch] instead of adding to it. The replacement is correct because the old ends[ch] already counted exactly the subsequences this same construction rebuilds: every subsequence that already ended in ch is reproduced, letter for letter, by appending ch to the subsequence left after removing that last ch, so it is one of the sum(ends) subsequences ends[ch] = sum(ends) + 1 just built, and keeping the old count too would count it a second time. The trap is writing ends[ch] += sum(ends) + 1: adding instead of replacing double-counts every repeat of a letter, so s = "aaa" returns more than the 3 distinct subsequences "a", "aa", "aaa".

See it run

Distinct Subsequences II with ends, step by step

letters read 0 of 3 · sum(ends) 0

letters read 0 of 3 · sum(ends) 0

letters read 1 of 3 · sum(ends) 1

letters read 1 of 3 · sum(ends) 1

letters read 2 of 3 · sum(ends) 3

letters read 2 of 3 · sum(ends) 3

letters read 3 of 3 · sum(ends) 6

letters read 3 of 3 · sum(ends) 6 · answer 6

Each column is one letter c. Each cell is ends[c], the number of distinct subsequences that end in c.

  • ch the letter being read
  • summed a count that is added into sum(ends)
  • replaced the count of ch, which gets the new value

Start: ends is all zeros. No subsequence has been built from s = "aba".

Read "a" at position 0: sum(ends) = 0 subsequences get "a" added, and "a" alone is one more. So the new count is 0 + 1 = 1.

ends["a"] = 1 replaces the old 0. The other letters keep their counts.

Read "b" at position 1: sum(ends) = 1 subsequences get "b" added, and "b" alone is one more. So the new count is 1 + 1 = 2.

ends["b"] = 2 replaces the old 0. The other letters keep their counts.

Read "a" at position 2: sum(ends) = 3 subsequences get "a" added, and "a" alone is one more. So the new count is 3 + 1 = 4.

ends["a"] = 4 replaces the old 1. The other letters keep their counts.

Done: the function returns sum(ends) % MOD = 6.

a0
b0
1 / 8
Gotchas1 of 3

Gotcha 1 of 3

  1. 1
    The trap

    Gotcha 1: Adding instead of replacing ends[ch] (the card's trap)

    Mistake
    ends[ord(ch) - ord("a")] = (sum(ends) + 1 + ends[ord(ch) - ord("a")]) % MOD
    Fix
    ends[ord(ch) - ord("a")] = (sum(ends) + 1) % MOD

    What happens: The old ends[ch] is already one of the sum(ends) subsequences this step rebuilds: every subsequence that already ended in ch is reproduced by appending ch to the subsequence left after removing its last ch. Adding the old value on top counts each of those a second time, so repeats of a letter inflate the answer.

    Example: s = "aaa"
    Mistake: returns 7Fix: returns 3
  2. 2

    Gotcha 2: Forgetting the + 1 for the one-letter subsequence

    Mistake
    ends[ord(ch) - ord("a")] = sum(ends) % MOD
    Fix
    ends[ord(ch) - ord("a")] = (sum(ends) + 1) % MOD

    What happens: The + 1 is the one-letter subsequence "ch" on its own, which exists the first time ch is read even though no earlier subsequence can be extended yet. Without it, a letter that never follows an existing subsequence is never counted at all.

    Example: s = "abc"
    Mistake: returns 0Fix: returns 7
  3. 3

    Gotcha 3: Forgetting the final % MOD on the return line

    Mistake
    return sum(ends)
    Fix
    return sum(ends) % MOD

    What happens: Each ends[c] is already reduced modulo 109 + 7 on every write, but summing 26 values that are each just below the modulus can still add up to a total past it. The per-letter reduction keeps every single ends[c] in range; it does not keep their sum in range, so the final % MOD is still needed on the line that adds them together.

    Example: s = "ab" repeated 100 times (200 characters)
    Mistake: returns 1407981065Fix: returns 407981058
Not this one if…
  • At each step you are in one of a few fixed modes (holding a stock or not) with simple rules for moving between them, as in Best Time to Buy and Sell Stock with Transaction Fee (LC 714): here there is no small fixed set of modes, only one running count per last letter, and a new letter replaces every one of those counts that it can finish, not just one mode's value. See State Machine DP
  • You are counting how many ways one target string t can be picked out of a second string s, as in Distinct Subsequences (LC 115): here there is no second string to match, so the count is of every distinct subsequence of s itself, keyed by last letter instead of by position in t. See Distinct Subsequences

Deep Dive

What each count means

A subsequence keeps some letters of s in their original order. It may skip letters. The problem asks for the number of different non-empty subsequences, so two choices of positions that spell the same text count once.

State Machine DP keeps one best value per mode. This card keeps one count per mode, and the mode is the last letter of the subsequence. ends[c] is the number of different non-empty subsequences built so far that end in the letter c. There are 26 modes.

Example: after reading "aba", ends for a is 4 and for b is 2. The six subsequences are a, aa, ba, aba (end in a, and there are 4 of them) and b, ab (end in b).

Why ends[ch] = sum(ends) + 1 is correct

When the letter ch is read, every subsequence built so far can be extended with ch. That gives sum(ends) new subsequences that end in ch. The letter ch alone is one more, so the total is sum(ends) + 1.

These new subsequences are all different from each other, because they come from different earlier subsequences or from the empty start. They also include every subsequence that already ended in ch. Such a subsequence is the earlier one with its last ch removed, plus ch.

So ends[ch] already counted a subset of the new group. The new value is the full group, and it replaces the old one. Adding to the old value would count that subset twice.

Example: reading "aaa" gives ends for a equal to 1, then 2, then 3. Adding instead gives 1, 3, 7, and the answer 7 is wrong.

Why the other letters stay unchanged

Reading ch does not remove any subsequence that was built before. A subsequence that ends in another letter d is still a valid subsequence of the longer string. So ends[d] keeps its value.

Only the subsequences that end in ch can be new. All of them are created in the single assignment to ends[ch]. The loop invariant holds again after each letter.

The answer is sum(ends) % MOD. Every different non-empty subsequence has exactly one last letter, so it is counted in exactly one entry of ends.

The cost and the modulus

The card's complexity line is O(26 * N) time and O(26) space. For each of the N letters, sum(ends) adds 26 numbers. 26 is a constant, so this is O(N) time and O(1) space.

The number of subsequences can reach 2^N - 1, so the problem asks for the result modulo 10^9 + 7. Python integers do not overflow, but the values would become very large. The code reduces each written value with % MOD.

Each ends[c] is below MOD, but the sum of 26 of them can be larger than MOD. So the return line still needs % MOD. Example: distinctSubseqII("ab" * 100) returns 407981058.

60-Second Pitch

"I need the number of different non-empty subsequences. I keep one count per last letter, ends[c]. When I read a letter ch, I can extend every subsequence built so far, and I can also start a new one with ch alone. So the new group ending in ch has sum(ends) + 1 members. That group already contains everything that ended in ch before, so I replace ends[ch] and I do not add to it. For "aaa" the counts are 1, 2, 3. I take everything modulo 10^9 + 7. The time is O(26 * N), which is O(N), and the space is O(26), which is O(1)."

Common Questions

Why do we replace ends[ch] and not add to it?

The new value sum(ends) + 1 already includes every subsequence that ended in ch before. Each of those is a shorter subsequence plus ch. Adding the old value counts them twice. For "aaa" the right answer is 3 and the adding version gives 7.

How would you count distinct subsequences with an O(N) table instead of 26 counts?

Let dp[i] be the number of distinct subsequences of the first i letters. Use dp[i] = 2 * dp[i - 1], and subtract dp[last - 1] when the letter appeared before at position last. This subtracts the subsequences that would be created twice. The 26 counts give the same result.

What changes if the alphabet is not limited to 26 letters?

Use a dictionary from letter to count, and keep a running total. Update the total with total = total - old + new. Then each letter costs O(1), and the time is O(N). The space is the number of different letters.

How is this different from Distinct Subsequences (LC 115)?

Distinct Subsequences (LC 115) counts how many ways a fixed string t appears inside s, so the state is a position in t. Here no target is given. We count every different subsequence of one string, so the state is the last letter.

What is the time and space complexity?

O(26 * N), which is O(N), time and O(26), which is O(1), space. Each letter does one sum over 26 numbers and one write. The result is reduced modulo 10^9 + 7.

SWE Reasoning

Spot it

  • You count something over one string, and no second string is given to match against.
  • The word "distinct" or "different" appears. Two choices of positions that give the same text must count once.
  • The brute force lists subsets of positions and puts the texts in a set. That is 2^N subsets, which is too slow for 2000 letters.
  • A new letter extends what exists, and the only fact you need about an old subsequence is its last letter.
  • The alphabet is small and fixed (26 letters), so one count per letter is cheap.

The invariant, step by step

  1. Before the loop, ends = [0] * 26. No subsequence has been built.
  2. A letter ch is read. Its index is ord(ch) - ord("a").
  3. Every subsequence built so far can be extended with ch. There are sum(ends) of them. The letter ch alone gives one more, so the new group has sum(ends) + 1 subsequences.
  4. This group contains every subsequence that already ended in ch, because each one is a shorter subsequence plus ch. So the old ends[ch] is replaced, not added to.
  5. The code writes ends[ch] = (sum(ends) + 1) % MOD. All other entries stay unchanged.
  6. After the last letter, each different non-empty subsequence is counted once, in the entry of its last letter. So sum(ends) % MOD is the answer.

Traps

  • Adding instead of replacing, on a string that is not one repeated letter. For "aba", the second a must give ends for a equal to 4. Adding the old value 1 gives 5 there, and the final answer is 7 instead of 6.
  • Counting the empty string. The problem asks for non-empty subsequences. If you count the empty string as one more subsequence and do not remove it at the end, "a" returns 2 instead of 1.
  • Using a set of strings. Storing every subsequence takes exponential memory. For a string of 2000 letters this cannot finish. Count by last letter instead.

Practice

Canonical LeetCode:Distinct Subsequences II (LC 940)Number of Unique Good Subsequences (LC 1987)
Theory Context•Dynamic Programming

Builds on State Machine DP, which you haven’t finished yet.

P3HardBuilt on State Machine DP~4 min speedrun

Last-Character Counting DP (Distinct Subsequences II)

Built on
State Machine DPThe mode is the last letter used, with up to 26 modes, and every count is updated together on each character.
The problem

Distinct Subsequences II (LC 940)(opens leetcode.com)

You get a string s of lowercase English letters. Deleting any number of its letters, possibly none, and keeping the rest in their original order gives a subsequence of s.

Count the different non-empty strings you can get this way. A string that several different choices of positions produce is counted once, not once per choice. The count can be very large, so return it modulo 109 + 7.

Example 1
Input:s = "abc"
Output:7
Every choice of letters gives a different string: "a", "b", "c", "ab", "ac", "bc" and "abc".

Last-Char Counting DP

s -> ends[26], one count per last letter -> replace on each new letter
Spot it when: You must count how many different non-empty subsequences a string has, with no second string to match against: count every subsequence of one string, not the ways to spell a fixed target.
Last-Char Counting DP
Python
1def distinctSubseqII(s: str) -> int:
2 """Counts the distinct non-empty subsequences of s, one running count per last letter.
3 Time: O(26 * N) = O(N) | Space: O(26) = O(1)
4 """
5 MOD = 10**9 + 7
6 ends = [0] * 26 # ends[c]: distinct non-empty subsequences built so far that end with letter c
7 for ch in s:
8 ends[ord(ch) - ord("a")] = (sum(ends) + 1) % MOD # Replace: the old ones ending in ch are rebuilt here
9 return sum(ends) % MOD
Invariant: After each letter of s is read, ends[c] is the number of different non-empty subsequences built so far that end in letter c, and every subsequence that already ended in c is already included inside that same count.

Keep one running count per last letter: ends[c] is how many different non-empty subsequences built so far end in c. A new letter ch can finish every one of those subsequences by appending ch to it, and it can also stand alone as the one-letter subsequence "ch", so ends[ch] = sum(ends) + 1 replaces the old ends[ch] instead of adding to it. The replacement is correct because the old ends[ch] already counted exactly the subsequences this same construction rebuilds: every subsequence that already ended in ch is reproduced, letter for letter, by appending ch to the subsequence left after removing that last ch, so it is one of the sum(ends) subsequences ends[ch] = sum(ends) + 1 just built, and keeping the old count too would count it a second time. The trap is writing ends[ch] += sum(ends) + 1: adding instead of replacing double-counts every repeat of a letter, so s = "aaa" returns more than the 3 distinct subsequences "a", "aa", "aaa".

See it run

Distinct Subsequences II with ends, step by step

letters read 0 of 3 · sum(ends) 0

letters read 0 of 3 · sum(ends) 0

letters read 1 of 3 · sum(ends) 1

letters read 1 of 3 · sum(ends) 1

letters read 2 of 3 · sum(ends) 3

letters read 2 of 3 · sum(ends) 3

letters read 3 of 3 · sum(ends) 6

letters read 3 of 3 · sum(ends) 6 · answer 6

Each column is one letter c. Each cell is ends[c], the number of distinct subsequences that end in c.

  • ch the letter being read
  • summed a count that is added into sum(ends)
  • replaced the count of ch, which gets the new value

Start: ends is all zeros. No subsequence has been built from s = "aba".

Read "a" at position 0: sum(ends) = 0 subsequences get "a" added, and "a" alone is one more. So the new count is 0 + 1 = 1.

ends["a"] = 1 replaces the old 0. The other letters keep their counts.

Read "b" at position 1: sum(ends) = 1 subsequences get "b" added, and "b" alone is one more. So the new count is 1 + 1 = 2.

ends["b"] = 2 replaces the old 0. The other letters keep their counts.

Read "a" at position 2: sum(ends) = 3 subsequences get "a" added, and "a" alone is one more. So the new count is 3 + 1 = 4.

ends["a"] = 4 replaces the old 1. The other letters keep their counts.

Done: the function returns sum(ends) % MOD = 6.

a0
b0
1 / 8
Gotchas1 of 3

Gotcha 1 of 3

  1. 1
    The trap

    Gotcha 1: Adding instead of replacing ends[ch] (the card's trap)

    Mistake
    ends[ord(ch) - ord("a")] = (sum(ends) + 1 + ends[ord(ch) - ord("a")]) % MOD
    Fix
    ends[ord(ch) - ord("a")] = (sum(ends) + 1) % MOD

    What happens: The old ends[ch] is already one of the sum(ends) subsequences this step rebuilds: every subsequence that already ended in ch is reproduced by appending ch to the subsequence left after removing its last ch. Adding the old value on top counts each of those a second time, so repeats of a letter inflate the answer.

    Example: s = "aaa"
    Mistake: returns 7Fix: returns 3
  2. 2

    Gotcha 2: Forgetting the + 1 for the one-letter subsequence

    Mistake
    ends[ord(ch) - ord("a")] = sum(ends) % MOD
    Fix
    ends[ord(ch) - ord("a")] = (sum(ends) + 1) % MOD

    What happens: The + 1 is the one-letter subsequence "ch" on its own, which exists the first time ch is read even though no earlier subsequence can be extended yet. Without it, a letter that never follows an existing subsequence is never counted at all.

    Example: s = "abc"
    Mistake: returns 0Fix: returns 7
  3. 3

    Gotcha 3: Forgetting the final % MOD on the return line

    Mistake
    return sum(ends)
    Fix
    return sum(ends) % MOD

    What happens: Each ends[c] is already reduced modulo 109 + 7 on every write, but summing 26 values that are each just below the modulus can still add up to a total past it. The per-letter reduction keeps every single ends[c] in range; it does not keep their sum in range, so the final % MOD is still needed on the line that adds them together.

    Example: s = "ab" repeated 100 times (200 characters)
    Mistake: returns 1407981065Fix: returns 407981058
Not this one if…
  • At each step you are in one of a few fixed modes (holding a stock or not) with simple rules for moving between them, as in Best Time to Buy and Sell Stock with Transaction Fee (LC 714): here there is no small fixed set of modes, only one running count per last letter, and a new letter replaces every one of those counts that it can finish, not just one mode's value. See State Machine DP
  • You are counting how many ways one target string t can be picked out of a second string s, as in Distinct Subsequences (LC 115): here there is no second string to match, so the count is of every distinct subsequence of s itself, keyed by last letter instead of by position in t. See Distinct Subsequences

Deep Dive

What each count means

A subsequence keeps some letters of s in their original order. It may skip letters. The problem asks for the number of different non-empty subsequences, so two choices of positions that spell the same text count once.

State Machine DP keeps one best value per mode. This card keeps one count per mode, and the mode is the last letter of the subsequence. ends[c] is the number of different non-empty subsequences built so far that end in the letter c. There are 26 modes.

Example: after reading "aba", ends for a is 4 and for b is 2. The six subsequences are a, aa, ba, aba (end in a, and there are 4 of them) and b, ab (end in b).

Why ends[ch] = sum(ends) + 1 is correct

When the letter ch is read, every subsequence built so far can be extended with ch. That gives sum(ends) new subsequences that end in ch. The letter ch alone is one more, so the total is sum(ends) + 1.

These new subsequences are all different from each other, because they come from different earlier subsequences or from the empty start. They also include every subsequence that already ended in ch. Such a subsequence is the earlier one with its last ch removed, plus ch.

So ends[ch] already counted a subset of the new group. The new value is the full group, and it replaces the old one. Adding to the old value would count that subset twice.

Example: reading "aaa" gives ends for a equal to 1, then 2, then 3. Adding instead gives 1, 3, 7, and the answer 7 is wrong.

Why the other letters stay unchanged

Reading ch does not remove any subsequence that was built before. A subsequence that ends in another letter d is still a valid subsequence of the longer string. So ends[d] keeps its value.

Only the subsequences that end in ch can be new. All of them are created in the single assignment to ends[ch]. The loop invariant holds again after each letter.

The answer is sum(ends) % MOD. Every different non-empty subsequence has exactly one last letter, so it is counted in exactly one entry of ends.

The cost and the modulus

The card's complexity line is O(26 * N) time and O(26) space. For each of the N letters, sum(ends) adds 26 numbers. 26 is a constant, so this is O(N) time and O(1) space.

The number of subsequences can reach 2^N - 1, so the problem asks for the result modulo 10^9 + 7. Python integers do not overflow, but the values would become very large. The code reduces each written value with % MOD.

Each ends[c] is below MOD, but the sum of 26 of them can be larger than MOD. So the return line still needs % MOD. Example: distinctSubseqII("ab" * 100) returns 407981058.

60-Second Pitch

"I need the number of different non-empty subsequences. I keep one count per last letter, ends[c]. When I read a letter ch, I can extend every subsequence built so far, and I can also start a new one with ch alone. So the new group ending in ch has sum(ends) + 1 members. That group already contains everything that ended in ch before, so I replace ends[ch] and I do not add to it. For "aaa" the counts are 1, 2, 3. I take everything modulo 10^9 + 7. The time is O(26 * N), which is O(N), and the space is O(26), which is O(1)."

Common Questions

Why do we replace ends[ch] and not add to it?

The new value sum(ends) + 1 already includes every subsequence that ended in ch before. Each of those is a shorter subsequence plus ch. Adding the old value counts them twice. For "aaa" the right answer is 3 and the adding version gives 7.

How would you count distinct subsequences with an O(N) table instead of 26 counts?

Let dp[i] be the number of distinct subsequences of the first i letters. Use dp[i] = 2 * dp[i - 1], and subtract dp[last - 1] when the letter appeared before at position last. This subtracts the subsequences that would be created twice. The 26 counts give the same result.

What changes if the alphabet is not limited to 26 letters?

Use a dictionary from letter to count, and keep a running total. Update the total with total = total - old + new. Then each letter costs O(1), and the time is O(N). The space is the number of different letters.

How is this different from Distinct Subsequences (LC 115)?

Distinct Subsequences (LC 115) counts how many ways a fixed string t appears inside s, so the state is a position in t. Here no target is given. We count every different subsequence of one string, so the state is the last letter.

What is the time and space complexity?

O(26 * N), which is O(N), time and O(26), which is O(1), space. Each letter does one sum over 26 numbers and one write. The result is reduced modulo 10^9 + 7.

SWE Reasoning

Spot it

  • You count something over one string, and no second string is given to match against.
  • The word "distinct" or "different" appears. Two choices of positions that give the same text must count once.
  • The brute force lists subsets of positions and puts the texts in a set. That is 2^N subsets, which is too slow for 2000 letters.
  • A new letter extends what exists, and the only fact you need about an old subsequence is its last letter.
  • The alphabet is small and fixed (26 letters), so one count per letter is cheap.

The invariant, step by step

  1. Before the loop, ends = [0] * 26. No subsequence has been built.
  2. A letter ch is read. Its index is ord(ch) - ord("a").
  3. Every subsequence built so far can be extended with ch. There are sum(ends) of them. The letter ch alone gives one more, so the new group has sum(ends) + 1 subsequences.
  4. This group contains every subsequence that already ended in ch, because each one is a shorter subsequence plus ch. So the old ends[ch] is replaced, not added to.
  5. The code writes ends[ch] = (sum(ends) + 1) % MOD. All other entries stay unchanged.
  6. After the last letter, each different non-empty subsequence is counted once, in the entry of its last letter. So sum(ends) % MOD is the answer.

Traps

  • Adding instead of replacing, on a string that is not one repeated letter. For "aba", the second a must give ends for a equal to 4. Adding the old value 1 gives 5 there, and the final answer is 7 instead of 6.
  • Counting the empty string. The problem asks for non-empty subsequences. If you count the empty string as one more subsequence and do not remove it at the end, "a" returns 2 instead of 1.
  • Using a set of strings. Storing every subsequence takes exponential memory. For a string of 2000 letters this cannot finish. Count by last letter instead.

Practice

Canonical LeetCode:Distinct Subsequences II (LC 940)Number of Unique Good Subsequences (LC 1987)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•