LeetCode 446 - Arithmetic Slices II - Subsequence
The problem asks us to count how many arithmetic subsequences exist inside a given integer array nums. An arithmetic sequence is a sequence where the difference between consecutive elements is constant. The sequence must contain at least three elements.
Difficulty: 🔴 Hard
Topics: Array, Dynamic Programming
Solution
Problem Understanding
The problem asks us to count how many arithmetic subsequences exist inside a given integer array nums.
An arithmetic sequence is a sequence where the difference between consecutive elements is constant. The sequence must contain at least three elements. Unlike subarrays, subsequences do not need to be contiguous. We are allowed to skip elements as long as the relative ordering remains unchanged.
For example, in the array [2,4,6,8,10], the subsequence [2,6,10] is valid because the consecutive differences are all 4, even though the elements are not adjacent in the original array.
The input is an integer array with length up to 1000. The values themselves can be as small as -2^31 and as large as 2^31 - 1. These constraints are important because:
- A brute-force subsequence enumeration is impossible, since an array of size
1000has2^1000subsequences. - Differences between two numbers may overflow a 32-bit integer, so we must use a wider integer type when computing differences.
- The result itself is guaranteed to fit inside a 32-bit integer.
The output should be the total number of arithmetic subsequences of length at least 3.
A critical detail is that subsequences of length 2 are not counted directly. However, they are extremely important during dynamic programming because longer arithmetic subsequences are built by extending shorter ones.
Several edge cases can easily break naive implementations:
- Arrays with all equal numbers produce many overlapping arithmetic subsequences.
- Negative differences must be handled correctly.
- Large integer values can cause overflow when computing differences.
- Arrays shorter than length
3always produce0. - Duplicate values create many distinct subsequences even when the numeric values look identical.
Approaches
Brute Force
A brute-force solution would generate every possible subsequence of the array, then check whether each subsequence is arithmetic.
To verify whether a subsequence is arithmetic, we compute the difference between adjacent elements and ensure all differences are identical. Any subsequence of length at least 3 satisfying this condition contributes to the answer.
This approach is correct because it explicitly examines every possible subsequence. However, it is computationally infeasible.
An array of length n has 2^n subsequences. With n = 1000, this becomes astronomically large. Even checking each subsequence efficiently would still be hopelessly slow.
Optimal Dynamic Programming Approach
The key observation is that arithmetic subsequences are defined entirely by:
- Their ending position
- Their common difference
This suggests a dynamic programming formulation.
Suppose we want to know how many arithmetic subsequences end at index i with common difference d.
If we look at some earlier index j < i, and:
nums[i] - nums[j] = d
then every arithmetic subsequence ending at j with difference d can be extended by nums[i].
Additionally, the pair (nums[j], nums[i]) itself forms a new subsequence of length 2, which may later grow into a valid arithmetic subsequence.
This leads to a DP structure where:
dp[i][d]stores the number of arithmetic subsequences of length at least2ending at indexiwith differenced- When extending an existing subsequence, we add to the final answer because the resulting subsequence now has length at least
3
This transforms the problem from exponential complexity into quadratic complexity.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(2^n * n) | O(n) | Enumerates every subsequence and checks arithmetic validity |
| Optimal Dynamic Programming | O(n^2) | O(n^2) | Tracks subsequence counts by ending index and difference |
Algorithm Walkthrough
- Create a DP array where each position contains a hash map.
Each dp[i] is a map:
difference -> count
The value represents how many arithmetic subsequences of length at least 2 end at index i with that difference.
2. Initialize the final answer to 0.
The answer only counts subsequences of length at least 3.
3. Iterate through every pair of indices (j, i) where j < i.
This pair represents a potential extension of arithmetic subsequences. 4. Compute the difference:
diff = nums[i] - nums[j]
We use a 64-bit integer because the subtraction may overflow a 32-bit integer.
5. Determine how many subsequences already end at j with this difference.
previous_count = dp[j][diff]
These are all arithmetic subsequences that can be extended using nums[i].
6. Add previous_count to the answer.
Every extension increases the subsequence length by one, so each becomes a valid arithmetic subsequence of length at least 3.
7. Update dp[i][diff].
We add:
previous_count, representing all extended subsequences1, representing the new length-2 subsequence(nums[j], nums[i])
So:
dp[i][diff] += previous_count + 1
- Continue until all pairs have been processed.
- Return the accumulated answer.
Why it works
The invariant is that dp[i][diff] always stores the number of arithmetic subsequences of length at least 2 ending at index i with common difference diff.
Whenever we process a pair (j, i):
- Every existing subsequence ending at
jwith differencediffcan be safely extended toi - Extending preserves the arithmetic property because the difference remains unchanged
- Every such extension creates a valid arithmetic subsequence of length at least
3
Since every arithmetic subsequence has a unique final index and difference, every valid subsequence is counted exactly once.
Python Solution
from collections import defaultdict
from typing import List
class Solution:
def numberOfArithmeticSlices(self, nums: List[int]) -> int:
n = len(nums)
# dp[i][diff] = number of arithmetic subsequences
# of length >= 2 ending at index i with difference diff
dp = [defaultdict(int) for _ in range(n)]
total_slices = 0
for i in range(n):
for j in range(i):
diff = nums[i] - nums[j]
previous_count = dp[j][diff]
# Extend all previous subsequences
total_slices += previous_count
# Add:
# 1 for the new pair (nums[j], nums[i])
# previous_count for all extended subsequences
dp[i][diff] += previous_count + 1
return total_slices
The implementation closely follows the dynamic programming strategy described earlier.
We first create a list of hash maps called dp. Each map tracks how many arithmetic subsequences end at a particular index for each difference value.
The outer loop selects the current ending index i. The inner loop considers every earlier index j, forming a pair (j, i).
For each pair, we compute the difference:
diff = nums[i] - nums[j]
We then look up how many arithmetic subsequences already ended at j using this same difference.
previous_count = dp[j][diff]
These subsequences can all be extended by appending nums[i].
Those extensions immediately contribute to the final answer because they now have length at least 3.
Finally, we update:
dp[i][diff] += previous_count + 1
The +1 accounts for the new subsequence of length 2 formed by (nums[j], nums[i]).
Go Solution
func numberOfArithmeticSlices(nums []int) int {
n := len(nums)
// dp[i][diff] = number of arithmetic subsequences
// of length >= 2 ending at index i with difference diff
dp := make([]map[int64]int, n)
for i := 0; i < n; i++ {
dp[i] = make(map[int64]int)
}
totalSlices := 0
for i := 0; i < n; i++ {
for j := 0; j < i; j++ {
diff := int64(nums[i]) - int64(nums[j])
previousCount := dp[j][diff]
// Extend previous subsequences
totalSlices += previousCount
// Add new pair and extended subsequences
dp[i][diff] += previousCount + 1
}
}
return totalSlices
}
The Go implementation mirrors the Python solution almost exactly.
The main difference is that Go requires explicit map initialization for every dp[i].
Another important difference is integer handling. Since subtracting two 32-bit integers may overflow, the difference is stored as int64:
diff := int64(nums[i]) - int64(nums[j])
Go maps return the zero value for missing keys automatically, which simplifies the lookup logic.
Worked Examples
Example 1
Input:
nums = [2,4,6,8,10]
We track:
dp[i][diff]
and the running answer.
| i | j | nums[i] | nums[j] | diff | previous_count | New dp[i][diff] | total |
|---|---|---|---|---|---|---|---|
| 1 | 0 | 4 | 2 | 2 | 0 | 1 | 0 |
| 2 | 0 | 6 | 2 | 4 | 0 | 1 | 0 |
| 2 | 1 | 6 | 4 | 2 | 1 | 2 | 1 |
| 3 | 0 | 8 | 2 | 6 | 0 | 1 | 1 |
| 3 | 1 | 8 | 4 | 4 | 0 | 1 | 1 |
| 3 | 2 | 8 | 6 | 2 | 2 | 3 | 3 |
| 4 | 0 | 10 | 2 | 8 | 0 | 1 | 3 |
| 4 | 1 | 10 | 4 | 6 | 0 | 1 | 3 |
| 4 | 2 | 10 | 6 | 4 | 1 | 2 | 4 |
| 4 | 3 | 10 | 8 | 2 | 3 | 4 | 7 |
Final answer:
7
The counted subsequences 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]
Example 2
Input:
nums = [7,7,7,7,7]
Every difference is 0.
| i | j | diff | previous_count | dp[i][0] | total |
|---|---|---|---|---|---|
| 1 | 0 | 0 | 0 | 1 | 0 |
| 2 | 0 | 0 | 0 | 1 | 0 |
| 2 | 1 | 0 | 1 | 3 | 1 |
| 3 | 0 | 0 | 0 | 1 | 1 |
| 3 | 1 | 0 | 1 | 3 | 2 |
| 3 | 2 | 0 | 3 | 7 | 5 |
| 4 | 0 | 0 | 0 | 1 | 5 |
| 4 | 1 | 0 | 1 | 3 | 6 |
| 4 | 2 | 0 | 3 | 7 | 9 |
| 4 | 3 | 0 | 7 | 15 | 16 |
Final answer:
16
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n^2) | Every pair of indices (j, i) is processed once |
| Space | O(n^2) | In the worst case, each index stores many distinct differences |
The nested loops examine all pairs of indices, producing quadratic time complexity.
The DP structure may also grow quadratically in the worst case because each pair of indices can introduce a distinct difference value.
Test Cases
from typing import List
class Solution:
def numberOfArithmeticSlices(self, nums: List[int]) -> int:
from collections import defaultdict
n = len(nums)
dp = [defaultdict(int) for _ in range(n)]
total = 0
for i in range(n):
for j in range(i):
diff = nums[i] - nums[j]
prev = dp[j][diff]
total += prev
dp[i][diff] += prev + 1
return total
sol = Solution()
assert sol.numberOfArithmeticSlices([2,4,6,8,10]) == 7
# Standard increasing arithmetic progression
assert sol.numberOfArithmeticSlices([7,7,7,7,7]) == 16
# All elements equal
assert sol.numberOfArithmeticSlices([1,2]) == 0
# Array too short
assert sol.numberOfArithmeticSlices([1,2,3]) == 1
# Single arithmetic subsequence
assert sol.numberOfArithmeticSlices([1,3,5,7,9]) == 7
# Multiple overlapping arithmetic subsequences
assert sol.numberOfArithmeticSlices([1,1,1]) == 1
# Minimal repeated-values case
assert sol.numberOfArithmeticSlices([1,1,1,1]) == 5
# Many duplicate-based subsequences
assert sol.numberOfArithmeticSlices([3,-1,-5,-9]) == 3
# Negative difference handling
assert sol.numberOfArithmeticSlices([1,5,9,13,17]) == 7
# Larger arithmetic progression
assert sol.numberOfArithmeticSlices([1,2,4,8,16]) == 0
# No valid arithmetic subsequences
assert sol.numberOfArithmeticSlices([0,2000000000,-294967296]) == 0
# Large values testing overflow safety
| Test | Why |
|---|---|
[2,4,6,8,10] |
Standard example with overlapping subsequences |
[7,7,7,7,7] |
Verifies handling of repeated values |
[1,2] |
Confirms arrays shorter than 3 return 0 |
[1,2,3] |
Smallest valid arithmetic subsequence |
[1,3,5,7,9] |
Multiple valid subsequences with same difference |
[1,1,1] |
Duplicate handling for minimal valid size |
[1,1,1,1] |
Stress test for repeated values |
[3,-1,-5,-9] |
Negative common differences |
[1,5,9,13,17] |
Longer arithmetic progression |
[1,2,4,8,16] |
No arithmetic subsequences |
[0,2000000000,-294967296] |
Overflow safety validation |
Edge Cases
Arrays Shorter Than Three Elements
An arithmetic subsequence must contain at least three elements. Arrays of length 0, 1, or 2 should always return 0.
This is easy to mishandle if the implementation accidentally counts pairs as valid arithmetic subsequences. The DP formulation avoids this bug because pairs are only stored as intermediate building blocks and are never directly added to the answer.
All Elements Equal
An array like:
[7,7,7,7,7]
creates a massive number of valid arithmetic subsequences because every subsequence has common difference 0.
This case stresses whether the implementation correctly accumulates counts rather than overwriting them. Using:
dp[i][diff] += previous_count + 1
ensures all overlapping subsequences are preserved.
Integer Overflow in Difference Calculation
The constraints allow values near the 32-bit integer limits.
For example:
nums[i] = 2147483647
nums[j] = -2147483648
Their difference exceeds the range of a signed 32-bit integer.
If the implementation stores differences in 32-bit integers, incorrect collisions may occur inside the hash map, producing invalid counts.
The solution avoids this problem by using Python's arbitrary precision integers and int64 in Go.