Files
codetutor/backend/data/questions/arithmetic-slices-ii-subsequence.yaml

234 lines
11 KiB
YAML

title: Arithmetic Slices II - Subsequence
slug: arithmetic-slices-ii-subsequence
difficulty: hard
leetcode_id: 446
leetcode_url: https://leetcode.com/problems/arithmetic-slices-ii-subsequence/
categories:
- arrays
- dynamic-programming
- hash-tables
patterns:
- dynamic-programming
function_signature: "def number_of_arithmetic_slices(nums: list[int]) -> int:"
test_cases:
visible:
- input: { nums: [2, 4, 6, 8, 10] }
expected: 7
- input: { nums: [7, 7, 7, 7, 7] }
expected: 16
hidden:
- input: { nums: [1] }
expected: 0
- input: { nums: [1, 2] }
expected: 0
- input: { nums: [1, 2, 3] }
expected: 1
- input: { nums: [1, 3, 5, 7] }
expected: 3
- input: { nums: [0, 0, 0, 0] }
expected: 5
- input: { nums: [1, 1, 1, 1, 1, 1] }
expected: 42
- input: { nums: [1, 2, 3, 4, 5, 6] }
expected: 12
description: |
Given an integer array `nums`, return *the number of all the **arithmetic subsequences** of* `nums`.
A sequence of numbers is called arithmetic if it consists of **at least three elements** and if the difference between any two consecutive elements is the same.
- For example, `[1, 3, 5, 7, 9]`, `[7, 7, 7, 7]`, and `[3, -1, -5, -9]` are arithmetic sequences.
- For example, `[1, 1, 2, 5, 7]` is not an arithmetic sequence.
A **subsequence** of an array is a sequence that can be formed by removing some elements (possibly none) of the array.
- For example, `[2, 5, 10]` is a subsequence of `[1, 2, 1, 2, 4, 1, 5, 10]`.
The test cases are generated so that the answer fits in a **32-bit** integer.
constraints: |
- `1 <= nums.length <= 1000`
- `-2^31 <= nums[i] <= 2^31 - 1`
examples:
- input: "nums = [2,4,6,8,10]"
output: "7"
explanation: "All arithmetic subsequence slices are: [2,4,6], [4,6,8], [6,8,10], [2,4,6,8], [4,6,8,10], [2,4,6,8,10], [2,6,10]"
- input: "nums = [7,7,7,7,7]"
output: "16"
explanation: "Any subsequence of this array is arithmetic since all elements are the same (difference = 0)."
explanation:
intuition: |
Imagine building arithmetic sequences piece by piece. The challenge is that a **subsequence** doesn't require consecutive elements — we can skip elements in the original array.
The key insight is that we need to track **partial sequences** of length 2 or more. Why? Because a valid arithmetic subsequence needs at least 3 elements, but to extend any sequence, we need to know what 2-element "building blocks" exist and what their common difference is.
Think of it like this: for each position `i` in the array, we maintain a record of "how many ways can I reach this position with a sequence ending here that has a specific difference `d`?" This includes both:
- Pairs (length 2) that could become valid if extended
- Longer sequences (length 3+) that are already valid
When we process a new element `nums[i]`, we look back at every previous element `nums[j]`. The difference `d = nums[i] - nums[j]` tells us which sequences can be extended. If there were `k` sequences ending at `j` with difference `d`, we can extend all of them by adding `nums[i]`.
The clever part: sequences of length 2 become length 3 (now valid and countable), and sequences already of length 3+ grow longer (still valid and countable). We count every extension of a sequence that was already length 2 or more.
approach: |
We use **Dynamic Programming with Hash Maps** to track subsequences by their common difference.
**Step 1: Set up the data structure**
- Create an array `dp` of hash maps, where `dp[i]` is a dictionary
- `dp[i][d]` represents the count of subsequences (of length >= 2) ending at index `i` with common difference `d`
- Initialise `result` to `0` to accumulate valid sequences (length >= 3)
&nbsp;
**Step 2: Iterate through all pairs**
- For each index `i` from `0` to `n-1`:
- For each previous index `j` from `0` to `i-1`:
- Calculate the difference `d = nums[i] - nums[j]`
&nbsp;
**Step 3: Extend existing subsequences**
- Look up how many subsequences end at `j` with difference `d` (call this `count_at_j`)
- Add `count_at_j` to `result` — these are all the sequences that just became length 3+, or were already valid and got extended
- Update `dp[i][d]` by adding `count_at_j + 1`:
- The `+1` accounts for the new pair `(nums[j], nums[i])` with difference `d`
- The `count_at_j` carries forward all extendable sequences from position `j`
&nbsp;
**Step 4: Return the result**
- Return `result`, which counts every valid arithmetic subsequence (length >= 3)
&nbsp;
The magic is that we only add to `result` when extending sequences that already have at least 2 elements at position `j`. This ensures we only count sequences of length 3 or more.
common_pitfalls:
- title: Confusing Subsequences with Subarrays
description: |
A **subarray** requires consecutive elements, but a **subsequence** can skip elements. For example, in `[2, 4, 6, 8, 10]`, the subsequence `[2, 6, 10]` is valid (difference of 4) even though the elements aren't adjacent.
This is why we need O(n^2) pairs — we must consider every possible pairing, not just adjacent elements.
wrong_approach: "Only checking consecutive elements"
correct_approach: "Check all pairs (i, j) where j < i"
- title: Counting Pairs as Valid Sequences
description: |
A valid arithmetic sequence needs **at least 3 elements**. A common mistake is counting pairs (length 2) as valid.
The solution handles this by only adding to `result` when we extend a sequence that already exists at `dp[j][d]`. A fresh pair `(nums[j], nums[i])` adds `1` to `dp[i][d]` but contributes `0` to `result` (since `dp[j][d]` was `0`).
wrong_approach: "Counting every pair with a common difference"
correct_approach: "Only count when extending existing sequences of length >= 2"
- title: Integer Overflow in Difference Calculation
description: |
With constraints `-2^31 <= nums[i] <= 2^31 - 1`, the difference `nums[i] - nums[j]` can overflow a 32-bit integer.
For example: `nums[i] = 2^31 - 1` and `nums[j] = -2^31` gives a difference of `2^32 - 1`, which exceeds 32-bit range.
In Python this isn't an issue (arbitrary precision integers), but in languages like Java or C++, you must use `long` for the difference calculation.
wrong_approach: "Using 32-bit integers for the difference"
correct_approach: "Use 64-bit integers (long) for difference calculations"
- title: Duplicate Elements Mishandling
description: |
When the array has duplicate values (e.g., `[7, 7, 7, 7, 7]`), many pairs share the same difference (0). Each pair can independently start or extend sequences.
With 5 identical elements, there are C(5,3) + C(5,4) + C(5,5) = 10 + 5 + 1 = 16 valid subsequences of length 3+. The DP correctly accumulates these because it processes every pair.
wrong_approach: "Skipping duplicate values"
correct_approach: "Process every pair regardless of duplicates"
key_takeaways:
- "**DP with hash maps**: When the state space (possible differences) is large or sparse, use hash maps instead of fixed-size arrays"
- "**Counting extensions**: Only count a sequence when it reaches the minimum valid length — track 'potential' sequences separately from 'valid' ones"
- "**O(n^2) for subsequences**: Unlike subarray problems that can often be solved in O(n), subsequence problems typically require considering all pairs"
- "**Foundation for sequence counting**: This pattern of tracking 'sequences ending at position i with property X' applies to many DP problems involving subsequences"
time_complexity: "O(n^2). We iterate through all pairs `(i, j)` where `j < i`, and hash map operations are O(1) on average."
space_complexity: "O(n^2). In the worst case, each position could have O(n) different differences stored in its hash map (e.g., when all elements are distinct)."
solutions:
- approach_name: Dynamic Programming with Hash Maps
is_optimal: true
code: |
def number_of_arithmetic_slices(nums: list[int]) -> int:
n = len(nums)
if n < 3:
return 0
# dp[i] maps difference -> count of subsequences ending at i
dp = [dict() for _ in range(n)]
result = 0
for i in range(n):
for j in range(i):
# Calculate the common difference
diff = nums[i] - nums[j]
# How many subsequences end at j with this difference?
count_at_j = dp[j].get(diff, 0)
# Add to result: these are valid sequences (length >= 3)
# or extensions of already valid sequences
result += count_at_j
# Update dp[i][diff]:
# - count_at_j sequences extended from j
# - +1 for the new pair (nums[j], nums[i])
dp[i][diff] = dp[i].get(diff, 0) + count_at_j + 1
return result
explanation: |
**Time Complexity:** O(n^2) — We examine all pairs of indices.
**Space Complexity:** O(n^2) — Each of the n hash maps can have up to O(n) entries.
The key insight is separating "potential" sequences (length 2) from "valid" sequences (length 3+). By only adding `count_at_j` to the result (not the `+1` for new pairs), we ensure we only count sequences that have reached the minimum length of 3.
- approach_name: Brute Force (Enumeration)
is_optimal: false
code: |
def number_of_arithmetic_slices(nums: list[int]) -> int:
from itertools import combinations
n = len(nums)
if n < 3:
return 0
count = 0
# Check all subsequences of length 3 or more
for length in range(3, n + 1):
for indices in combinations(range(n), length):
# Extract the subsequence
subseq = [nums[i] for i in indices]
# Check if it's arithmetic
if is_arithmetic(subseq):
count += 1
return count
def is_arithmetic(seq: list[int]) -> bool:
if len(seq) < 3:
return False
diff = seq[1] - seq[0]
for i in range(2, len(seq)):
if seq[i] - seq[i - 1] != diff:
return False
return True
explanation: |
**Time Complexity:** O(2^n * n) — We enumerate all subsequences (2^n) and check each one (O(length)).
**Space Complexity:** O(n) — For storing each subsequence during checking.
This approach generates every possible subsequence of length 3 or more and checks if it's arithmetic. While correct, it's far too slow for the given constraints (n up to 1000). With n=1000, there are approximately 2^1000 subsequences — completely infeasible. This illustrates why the DP approach is necessary.