From c6790e823aff926719290a9932e90ca57f74a6bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=BD=20Xu=C3=A2n=20Sang?= Date: Mon, 7 Sep 2026 09:37:00 +0700 Subject: [PATCH] add 940 solution --- .../description.md | 38 ++++ Hard/940.Distinct-Subsequences-II/solution.md | 187 ++++++++++++++++++ README.md | 5 +- SUMMARY.md | 1 + _sidebar.md | 1 + 5 files changed, 230 insertions(+), 2 deletions(-) create mode 100644 Hard/940.Distinct-Subsequences-II/description.md create mode 100644 Hard/940.Distinct-Subsequences-II/solution.md diff --git a/Hard/940.Distinct-Subsequences-II/description.md b/Hard/940.Distinct-Subsequences-II/description.md new file mode 100644 index 0000000..2abff07 --- /dev/null +++ b/Hard/940.Distinct-Subsequences-II/description.md @@ -0,0 +1,38 @@ +# 940. 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** `10^9 + 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 +`"abcde"` while `"aec"` is not.) + +## Example 1 + +```text +Input: s = "abc" +Output: 7 +Explanation: The 7 distinct subsequences are "a", "b", "c", "ab", "ac", "bc", and "abc". +``` + +## Example 2 + +```text +Input: s = "aba" +Output: 6 +Explanation: The 6 distinct subsequences are "a", "b", "ab", "aa", "ba", and "aba". +``` + +## Example 3 + +```text +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. diff --git a/Hard/940.Distinct-Subsequences-II/solution.md b/Hard/940.Distinct-Subsequences-II/solution.md new file mode 100644 index 0000000..8eeb667 --- /dev/null +++ b/Hard/940.Distinct-Subsequences-II/solution.md @@ -0,0 +1,187 @@ +# Intuition + +Sweep `s` left to right and carry one number: how many distinct subsequences the +prefix has, counting the empty one. When the next character `c` arrives, every +subsequence counted so far either ignores `c` or appends it, which suggests +doubling the total. Appending really does produce `total` *distinct* strings — +adding the same character to distinct strings cannot collide — so the only +question is how many of those strings were already counted. + +The answer is exactly the strings that the **previous occurrence of `c`** created. +Writing $$T_i$$ for the set of distinct subsequences of the first `i` characters +and $$T_i \cdot c$$ for the set with `c` appended to each element: + +$$T_i = T_{i-1} \cup (T_{i-1} \cdot c), \qquad (T_{i-1} \cdot c) \cap T_{i-1} = T_{j-1} \cdot c$$ + +where `j` is the previous index at which `c` occurred. A subsequence of the prefix +that ends in `c` must have been formed at some earlier occurrence of `c`, and the +most recent one already generated all of them. So + +$$|T_i| = 2\,|T_{i-1}| - |T_{j-1}|$$ + +and remembering one number per letter — the total as it stood just before that +letter was last consumed — is enough. Twenty-six counters replace the whole DP +table. + +# Approach: Running Total With Per-Letter Memory + +1. Start with `total = 1` (the empty subsequence) and `last[26]` all zero, so a + letter never seen before subtracts nothing. +2. For each character `c` of `s`: + - `new_total = 2 * total - last[c]` + - `last[c] = total` — the total *before* this step + - `total = new_total` +3. Return `total - 1`, dropping the empty subsequence. + +## Why the temporary is not optional + +`last[c]` must record $$|T_{i-1}|$$, the total from *before* the update. Folding +the two assignments together — updating `total` first and then storing it into +`last[c]` — records $$|T_i|$$ instead and subtracts too much on the next +occurrence of that letter. It fails on the smallest repeat: + +| `s` | correct | with the assignments swapped | +| ---------- | ------- | ---------------------------- | +| `"aba"` | `6` | `5` | +| `"aaa"` | `3` | `1` | +| `"abab"` | `11` | `7` | + +That is what the `new_total` / `newTotal` variable is buying: the three lines are +a simultaneous update, not a sequence of three independent ones. + +# Worked example: `s = "aba"` → `6` + +| step | `c` | `2 * total` | `last[c]` | new `total` | $$T_i$$ | +| ---- | --- | ----------- | --------- | ----------- | --------------------------- | +| 0 | — | — | — | `1` | `{""}` | +| 1 | `a` | `2` | `0` | `2` | `{"", "a"}` | +| 2 | `b` | `4` | `0` | `4` | `{"", "a", "b", "ab"}` | +| 3 | `a` | `8` | `1` | `7` | the four above, plus `"aa"`, `"ba"`, `"aba"` | + +Step 3 is the whole idea in miniature. Appending `a` to +`{"", "a", "b", "ab"}` yields `{"a", "aa", "ba", "aba"}`, and `"a"` is already in +the set — one duplicate, which is precisely `last[a] = 1`, the total back when the +first `a` was consumed. The answer is `7 - 1 = 6`, matching Example 2. + +# Modular arithmetic: the `+ MOD` is load-bearing + +Two details make the difference between a correct submission and a subtly wrong +one, and both are invisible on small inputs. + +**The running total can go negative in Go and Rust.** Those languages define `%` +by truncation toward zero, so a negative dividend gives a negative remainder. +Once `total` has wrapped past $$10^9 + 7$$ it can be small while `last[c]` — an +older, unrelated residue — is large, making `2 * total - last[c]` negative and +leaving `total` negative. This is not hypothetical: across the 10153-case corpus +below it happens on 228 of them, with the running total dipping to +`-999973602`. It cannot happen before the total first exceeds the modulus, which +takes about 30 doublings, and a search over random 26-letter strings found it +starting as early as the 31st character — far enough in that hand-checked +examples never reach it. Python is immune: its `%` floors, so `total` stays in +$$[0, 10^9 + 7)$$, and there the `+ MOD` is merely belt and braces. + +**One `+ MOD` is enough, but only just.** Because `%` bounds the magnitude, +`total` always satisfies $$-(10^9 + 7) < total < 10^9 + 7$$, so `total - 1 + MOD` +lands in $$[0, 2 \cdot (10^9 + 7))$$ and the final `% MOD` normalises it. Every +operation on the way is an addition or subtraction, so each residue stays +congruent to the true count and the normalised result is exact. Drop the `+ MOD` +and a legitimate input whose count is a multiple of the modulus returns `-1`. + +**Width.** Since $$|2 \cdot total - last[c]| < 3 \cdot (10^9 + 7) \approx 3 \times 10^9$$, +the arithmetic needs 64 bits. That is why the Rust version accumulates in `i64` +and casts only at the end, where the normalised value is below $$2^{31}$$ and the +cast is lossless; a bare `i32` accumulator would overflow. Go's `int` is 64-bit on +the judge, so it is safe as written. + +# Complexity + +- Time complexity: $$O(n)$$, where `n` is the length of `s` — one pass, constant + work per character. +- Space complexity: $$O(1)$$ — 26 counters, independent of `n`. + +# Code + +## Go + +```go +const MOD = 1_000_000_000 + 7 + +func distinctSubseqII(s string) int { + last := [26]int{} + total := 1 + for _, ch := range s { + c := ch - 'a' + newTotal := (2 * total - last[c]) % MOD + last[c] = total + total = newTotal + } + return (total - 1 + MOD) % MOD +} +``` + +`range` over a `string` yields runes, so `c` is a `rune` used directly as an array +index — legal, and correct here because the constraints promise lowercase ASCII. + +## Rust + +```rust +const MOD: i64 = 1_000_000_000 + 7; + +impl Solution { + pub fn distinct_subseq_ii(s: String) -> i32 { + let mut last = [0; 26]; + let mut total = 1_i64; + for ch in s.as_bytes() { + let c = (ch - b'a') as usize; + let new_total = (2 * total - last[c]) % MOD; + last[c] = total; + total = new_total; + } + ((total - 1 + MOD) % MOD) as i32 + } +} +``` + +`last` is inferred as `[i64; 26]` from `last[c] = total`, which keeps the whole +computation in 64 bits. + +## Python + +```python +MOD = 1_000_000_000 + 7 + +class Solution: + def distinctSubseqII(self, s: str) -> int: + total, last = 1, [0] * 26 + for ch in s: + c = ord(ch) - ord('a') + new_total = (2 * total - last[c]) % MOD + last[c] = total + total = new_total + return (total - 1 + MOD) % MOD +``` + +# Test cases + +| `s` | answer | why | +| --------------- | ----------- | -------------------------------------------- | +| `"abc"` | `7` | Example 1 — all distinct, $$2^3 - 1$$ | +| `"aba"` | `6` | Example 2 — one duplicate removed | +| `"aaa"` | `3` | Example 3 — only `"a"`, `"aa"`, `"aaa"` | +| `"a"` | `1` | shortest possible input | +| `"aa"` | `2` | first repeat, smallest subtraction | +| `"abab"` | `11` | catches swapped `last[c]` / `total` updates | +| `"a" * 2000` | `2000` | maximal input, minimal answer | +| `"ab" * 1000` | `694708213` \* | maximal input where the total wraps repeatedly | + +\* the modular residue; the true count is astronomically larger. + +All three implementations were checked against an exact big-integer reference — +the same recurrence with unbounded integers, reduced modulo $$10^9 + 7$$ only at +the very end — on a shared corpus of 10153 cases: the three examples, every string +over `{a,b,c}` of length up to 8, 300 random strings up to the maximum length 2000 +over alphabets of 1, 2, 3, 10 and 26 letters, and six structured maximal inputs. +All agreed on every case. The reference was itself validated against a brute force +that materialises the set of subsequences, on all 9851 cases short enough to +enumerate. The Rust build had overflow checks enabled and never tripped them, +confirming the 64-bit accumulator claim. diff --git a/README.md b/README.md index 6293239..d551490 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Easy/350.Intersection-of-Two-Arrays-II/ ## Solutions index -Total: **213** problems with at least one solution file. +Total: **214** problems with at least one solution file. Solution links use variant names when multiple approaches or languages exist (`main` = `solution.md`, others = `solution-.md`). @@ -211,7 +211,7 @@ Solution links use variant names when multiple approaches or languages exist (`m | 3876. Construct Uniform Parity Array II | [Link](https://leetcode.com/problems/construct-uniform-parity-array-ii/) | [main](Medium/3876.Construct-Uniform-Parity-Array-II/solution.md) | | 3904. Smallest Stable Index II | [Link](https://leetcode.com/problems/smallest-stable-index-ii/) | [main](Medium/3904.Smallest-Stable-Index-II/solution.md) | -### Hard (35) +### Hard (36) | Problem | LeetCode | Solution | | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | @@ -231,6 +231,7 @@ Solution links use variant names when multiple approaches or languages exist (`m | 726. Number of Atoms | [Link](https://leetcode.com/problems/number-of-atoms/) | [go](Hard/726.Number-of-Atoms/solution-go.md) · [main](Hard/726.Number-of-Atoms/solution.md) | | 773. Sliding Puzzle | [Link](https://leetcode.com/problems/sliding-puzzle/) | [main](Hard/773.Sliding-Puzzle/solution.md) | | 862. Shortest Subarray with Sum at Least K | [Link](https://leetcode.com/problems/shortest-subarray-with-sum-at-least-k/) | [main](Hard/862.Shortest-Subarray-with-Sum-at-Least-K/solution.md) | +| 940. Distinct Subsequences II | [Link](https://leetcode.com/problems/distinct-subsequences-ii/) | [main](Hard/940.Distinct-Subsequences-II/solution.md) | | 995. Minimum Number of K Consecutive Bit Flips | [Link](https://leetcode.com/problems/minimum-number-of-k-consecutive-bit-flips/) | [main](Hard/995.Minimum-Number-of-K-Consecutive-Bit-Flips/solution.md) | | 1106. Parsing A Boolean Expression | [Link](https://leetcode.com/problems/parsing-a-boolean-expression/) | [main](Hard/1106.Parsing-A-Boolean-Expression/solution.md) | | 1301. Number of Paths with Max Score | [Link](https://leetcode.com/problems/number-of-paths-with-max-score/) | [main](Hard/1301.Number-of-Paths-with-Max-Score/solution.md) | diff --git a/SUMMARY.md b/SUMMARY.md index 247b7b9..e141e63 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -220,6 +220,7 @@ * [726. Number of Atoms](Hard/726.Number-of-Atoms/solution.md) * [773. Sliding Puzzle](Hard/773.Sliding-Puzzle/solution.md) * [862. Shortest Subarray with Sum at Least K](Hard/862.Shortest-Subarray-with-Sum-at-Least-K/solution.md) +* [940. Distinct Subsequences II](Hard/940.Distinct-Subsequences-II/solution.md) * [995. Minimum Number of K Consecutive Bit Flips](Hard/995.Minimum-Number-of-K-Consecutive-Bit-Flips/solution.md) * [1106. Parsing A Boolean Expression](Hard/1106.Parsing-A-Boolean-Expression/solution.md) * [1301. Number of Paths with Max Score](Hard/1301.Number-of-Paths-with-Max-Score/solution.md) diff --git a/_sidebar.md b/_sidebar.md index d1f003c..2abd760 100644 --- a/_sidebar.md +++ b/_sidebar.md @@ -213,6 +213,7 @@ - [726. Number of Atoms](Hard/726.Number-of-Atoms/solution.md) - [773. Sliding Puzzle](Hard/773.Sliding-Puzzle/solution.md) - [862. Shortest Subarray with Sum at Least K](Hard/862.Shortest-Subarray-with-Sum-at-Least-K/solution.md) + - [940. Distinct Subsequences II](Hard/940.Distinct-Subsequences-II/solution.md) - [995. Minimum Number of K Consecutive Bit Flips](Hard/995.Minimum-Number-of-K-Consecutive-Bit-Flips/solution.md) - [1106. Parsing A Boolean Expression](Hard/1106.Parsing-A-Boolean-Expression/solution.md) - [1301. Number of Paths with Max Score](Hard/1301.Number-of-Paths-with-Max-Score/solution.md)