Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions problems/0940-distinct-subsequences-ii/analysis_daily_20260907.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# 0940. Distinct Subsequences II

[LeetCode Link](https://leetcode.com/problems/distinct-subsequences-ii/)

Difficulty: Hard
Topics: String, Dynamic Programming
Acceptance Rate: 47.8%

## Hints

### Hint 1

Enumerating subsequences is hopeless: a string of length 2000 has up to `2^2000` of them, and the problem asks for an answer modulo `10^9 + 7` precisely because it wants you to *count* rather than *build*. Whenever you must count objects that are built one character at a time, think about scanning the string left to right and maintaining a running count that you can extend by one character at each step. That is the shape of a dynamic programming solution.

The hard part is not the counting — it is the word **distinct**. Try the naive recurrence "each new character doubles the count, plus one for the character alone" on `"aa"` and see exactly where it breaks.

### Hint 2

A single scalar `dp[i] = number of distinct subsequences of s[0..i]` is not enough state. When you append a character `c`, you need to know how many of the subsequences you already counted would collide with the new ones you are about to create — and collisions only happen with subsequences that *end in `c`*.

So refine your state: instead of one number, keep 26 numbers, one per lowercase letter. Ask yourself what the natural definition is such that appending `c` only touches the bucket for `c`.

### Hint 3

Define `dp[c]` = the number of distinct non-empty subsequences of the prefix processed so far that **end with the character `c`**.

Now the recurrence is clean. When you process a new character `c`, every distinct subsequence seen so far can be extended by `c`, and `c` can also stand alone:

```
dp[c] = (dp['a'] + dp['b'] + ... + dp['z']) + 1
```

Note this is an **assignment, not an addition**. The new value *replaces* the old `dp[c]`, and that replacement is exactly what removes the duplicates: any subsequence ending in `c` that existed before is regenerated by this formula (it was some prefix subsequence plus a `c`, or the bare `"c"`), so counting it again would double-count. Overwriting keeps each distinct string counted once, attributed to its *last* occurrence of `c`.

The answer is the sum of all 26 buckets at the end. Every distinct subsequence ends in exactly one letter, so the buckets partition the answer set — no double counting across buckets.

## Approach

Scan the string once, left to right, maintaining an array `dp[26]` where `dp[c]` is the number of distinct non-empty subsequences of the prefix seen so far whose **last character** is `c`.

Because each distinct subsequence has exactly one last character, these 26 buckets are a partition of the set we want to count, and the answer is `sum(dp)`.

**The transition.** Suppose we have processed some prefix and now read character `c`. Which distinct subsequences of the new prefix end with `c`? Each such string is `t + c` where `t` is either empty or a distinct subsequence of the prefix. So the count is:

```
dp[c] = total + 1 where total = sum(dp[0..25]) before this step
```

The `+1` accounts for `t` being empty, i.e. the single-character string `"c"`.

**Why overwriting is correct.** This is the crux. If `c` appeared earlier, `dp[c]` already held a value. But every string counted there is also counted by the new formula: it had the form `t + c` for some `t` that was a subsequence of an even earlier prefix, and any subsequence of an earlier prefix is still a subsequence of the current one. So the new value is a *superset* count, and adding instead of assigning would count those strings twice. Assignment canonicalizes each distinct subsequence to the **last** position of its final character.

**Worked example: `s = "aba"`.**

| step | char | `total` before | update | `dp[a]` | `dp[b]` |
|------|------|---------------|--------|---------|---------|
| init | — | 0 | — | 0 | 0 |
| 1 | `a` | 0 | `dp[a] = 0 + 1` | 1 | 0 |
| 2 | `b` | 1 | `dp[b] = 1 + 1` | 1 | 2 |
| 3 | `a` | 3 | `dp[a] = 3 + 1` | 4 | 2 |

Answer = `4 + 2 = 6`. The bucket for `a` holds `{"a", "aa", "ba", "aba"}` and the bucket for `b` holds `{"b", "ab"}` — exactly the six strings the problem lists, with no duplicates. Notice how step 3 overwrote the `1` from step 1: the string `"a"` is counted once, attributed to the second `a`.

Contrast with `s = "aaa"`: step 1 sets `dp[a] = 1`, step 2 sets `dp[a] = 1 + 1 = 2`, step 3 sets `dp[a] = 2 + 1 = 3`. The answer is `3`, not `7`, because the overwrite discards the stale counts each time.

**Modular arithmetic.** All values are kept modulo `10^9 + 7`. Since we only ever add, no subtraction is involved and no negative-value fixup is needed. (A common alternative formulation uses `dp[i] = 2*dp[i-1] - dp[last[c]-1]`, which *does* subtract and therefore needs a `(x % M + M) % M` correction — the 26-bucket version avoids that trap entirely.)

Summing 26 buckets on every character makes the scan `O(26n)`. You can drop the inner loop to get a true `O(n)` by carrying `total` as a running variable: when you overwrite `dp[c]` with `newVal`, update `total += newVal - dp[c]`. That reintroduces a subtraction, so it needs the modular fixup; the version below keeps the explicit 26-element sum for clarity, and at `n <= 2000` the difference is irrelevant.

## Complexity Analysis

Time Complexity: O(26n) = O(n), where n is the length of `s`. One pass over the string, with a constant 26-element sum per character. For n = 2000 this is roughly 52,000 additions.

Space Complexity: O(1) — a fixed array of 26 counters, independent of the input length.

## Edge Cases

- **Single character (`"a"`)** — the smallest legal input per the constraints (`1 <= s.length`). The answer is `1`. Verifies that the `+1` term fires correctly on an empty `dp`.
- **All identical characters (`"aaa"`)** — the case that kills the naive "double it" recurrence, which would return `7`. The correct answer is `3` (`"a"`, `"aa"`, `"aaa"`). This is the single best test for whether your dedup logic works.
- **A repeated character that is not adjacent (`"aba"`)** — checks that you overwrite based on the character's own bucket rather than on adjacency. Answer `6`, not `7`.
- **All distinct characters (`"abc"`)** — no collisions at all, so the count really is `2^n - 1`. Useful as a sanity check that you are not *over*-deduplicating.
- **The full alphabet** — `2^26 - 1 = 67108863`, still under the modulus, so it validates the counting without the modulus interfering.
- **Overflow / modulus** — with n = 2000 the true count is astronomically larger than any 64-bit integer, so you must reduce at every step, not just at the end. Take the modulus both when writing `dp[c]` and when accumulating the sum.
- **Empty string** — not reachable under the stated constraints, but a robust implementation should return `0` rather than panic, since the count excludes the empty subsequence.

This is a genuinely hard problem, and the difficulty is concentrated in one line: recognizing that `dp[c]` must be *assigned* rather than *incremented*. Once that clicks the code is about ten lines. If it did not click on your own, that is normal — the "bucket by last character" trick is a reusable pattern (it also shows up in counting distinct subsequences of a given target and in string-DP dedup problems generally), so it is worth committing to memory.
55 changes: 55 additions & 0 deletions problems/0940-distinct-subsequences-ii/problem.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
number: "0940"
frontend_id: "940"
title: "Distinct Subsequences II"
slug: "distinct-subsequences-ii"
difficulty: "Hard"
topics:
- "String"
- "Dynamic Programming"
acceptance_rate: 4781.3
is_premium: false
created_at: "2026-09-07T05:00:21.946171+00:00"
fetched_at: "2026-09-07T05:00:21.946171+00:00"
link: "https://leetcode.com/problems/distinct-subsequences-ii/"
date: "2026-09-07"
---

# 0940. Distinct Subsequences II

Given a string s, return _the number of**distinct non-empty subsequences** of_ `s`. Since the answer may be very large, return it **modulo** `109 + 7`.

A **subsequence** of a string is a new string that is formed from the original string by deleting some (can be none) of the characters without disturbing the relative positions of the remaining characters. (i.e., `"ace"` is a subsequence of `"_a_ b _c_ d _e_ "` while `"aec"` is not.



**Example 1:**


**Input:** s = "abc"
**Output:** 7
**Explanation:** The 7 distinct subsequences are "a", "b", "c", "ab", "ac", "bc", and "abc".


**Example 2:**


**Input:** s = "aba"
**Output:** 6
**Explanation:** The 6 distinct subsequences are "a", "b", "ab", "aa", "ba", and "aba".


**Example 3:**


**Input:** s = "aaa"
**Output:** 3
**Explanation:** The 3 distinct subsequences are "a", "aa" and "aaa".




**Constraints:**

* `1 <= s.length <= 2000`
* `s` consists of lowercase English letters.
47 changes: 47 additions & 0 deletions problems/0940-distinct-subsequences-ii/solution_daily_20260907.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package main

// 0940. Distinct Subsequences II
// https://leetcode.com/problems/distinct-subsequences-ii/
//
// Approach: dynamic programming with one bucket per lowercase letter.
//
// dp[c] holds the number of distinct non-empty subsequences of the prefix
// processed so far that END with the character c. Every distinct subsequence
// ends in exactly one letter, so the 26 buckets partition the answer set and
// the result is simply their sum.
//
// When we read a character c, the distinct subsequences of the new prefix
// ending in c are exactly {t + c} for t empty or any distinct subsequence of
// the prefix, giving:
//
// dp[c] = (sum of all dp) + 1
//
// The assignment (rather than +=) is what removes duplicates: any subsequence
// ending in c that was already counted is regenerated by this formula, so
// overwriting attributes each distinct string to the LAST occurrence of its
// final character and counts it exactly once.
//
// Time: O(26n). Space: O(1).

const distinctSubseqIIMod = 1_000_000_007

func distinctSubseqII(s string) int {
// dp[c] = count of distinct subsequences seen so far ending in 'a'+c.
var dp [26]int

for i := 0; i < len(s); i++ {
total := 0
for _, count := range dp {
total = (total + count) % distinctSubseqIIMod
}
// Overwrite, do not accumulate: the old value is already included
// in total, and re-adding it would double count.
dp[s[i]-'a'] = (total + 1) % distinctSubseqIIMod
}

answer := 0
for _, count := range dp {
answer = (answer + count) % distinctSubseqIIMod
}
return answer
}
138 changes: 138 additions & 0 deletions problems/0940-distinct-subsequences-ii/solution_daily_20260907_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package main

import (
"strings"
"testing"
)

func TestSolution(t *testing.T) {
tests := []struct {
name string
s string
expected int
}{
{
name: "example 1: all distinct characters, 2^3-1 subsequences",
s: "abc",
expected: 7,
},
{
name: "example 2: repeated non-adjacent character forces dedup",
s: "aba",
expected: 6,
},
{
name: "example 3: all identical characters collapse to length count",
s: "aaa",
expected: 3,
},
{
name: "edge case: single character is the smallest legal input",
s: "a",
expected: 1,
},
{
name: "edge case: empty string has no non-empty subsequences",
s: "",
expected: 0,
},
{
name: "edge case: two identical characters, not 3",
s: "zz",
expected: 2,
},
{
name: "edge case: interleaved repeats abab",
s: "abab",
expected: 11,
},
{
name: "edge case: full alphabet gives exactly 2^26-1",
s: "abcdefghijklmnopqrstuvwxyz",
expected: 67108863,
},
{
name: "edge case: alphabet plus a repeated 'a' gives 2^27-2",
s: "abcdefghijklmnopqrstuvwxyza",
expected: 134217726,
},
{
name: "edge case: max length all identical characters",
s: strings.Repeat("a", 2000),
expected: 2000,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := distinctSubseqII(tt.s)
if result != tt.expected {
t.Errorf("distinctSubseqII(%q) = %v, want %v", truncate(tt.s), result, tt.expected)
}
})
}
}

// TestSolutionMatchesBruteForce cross-checks the DP against an exhaustive
// set-based enumeration on every short string over a small alphabet.
func TestSolutionMatchesBruteForce(t *testing.T) {
alphabet := []byte{'a', 'b', 'c'}

var walk func(prefix []byte)
walk = func(prefix []byte) {
if len(prefix) > 0 {
s := string(prefix)
want := bruteForceDistinctSubseq(s)
if got := distinctSubseqII(s); got != want {
t.Errorf("distinctSubseqII(%q) = %v, want %v", s, got, want)
}
}
if len(prefix) == 8 {
return
}
for _, c := range alphabet {
walk(append(prefix, c))
}
}
walk(nil)
}

// TestSolutionLongInputStaysInModulus verifies that the count is reduced at
// every step: the true answer for a 2000-character string overflows any fixed
// width integer, so the result must remain a valid residue.
func TestSolutionLongInputStaysInModulus(t *testing.T) {
s := strings.Repeat("abcdefghijklmnopqrstuvwxyz", 77) // 2002 chars, trimmed below
s = s[:2000]

got := distinctSubseqII(s)
if got < 0 || got >= 1_000_000_007 {
t.Errorf("distinctSubseqII(<2000 chars>) = %v, want a value in [0, 1000000007)", got)
}
if got == 0 {
t.Errorf("distinctSubseqII(<2000 chars>) = 0, which is almost certainly a bug rather than a genuine residue")
}
}

// bruteForceDistinctSubseq enumerates every subsequence into a set. Only usable
// for very short strings (2^len subsets).
func bruteForceDistinctSubseq(s string) int {
seen := make(map[string]struct{})
for mask := 1; mask < 1<<len(s); mask++ {
var sb strings.Builder
for i := 0; i < len(s); i++ {
if mask&(1<<i) != 0 {
sb.WriteByte(s[i])
}
}
seen[sb.String()] = struct{}{}
}
return len(seen)
}

// truncate keeps failure messages readable for very long inputs.
func truncate(s string) string {
if len(s) <= 40 {
return s
}
return s[:40] + "..."
}