Skip to content
Merged
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
38 changes: 38 additions & 0 deletions Hard/940.Distinct-Subsequences-II/description.md
Original file line number Diff line number Diff line change
@@ -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.
187 changes: 187 additions & 0 deletions Hard/940.Distinct-Subsequences-II/solution.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<variant>.md`).

Expand Down Expand Up @@ -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 |
| ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
Expand All @@ -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) |
Expand Down
1 change: 1 addition & 0 deletions SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions _sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading