Facebook Pixel

446. Arithmetic Slices II - Subsequence

Problem Description

This problem asks you to count the total number of arithmetic subsequences in an integer array nums.

An arithmetic sequence must satisfy two conditions:

  1. It contains at least 3 elements
  2. The difference between any two consecutive elements is the same (constant difference)

For example:

  • [1, 3, 5, 7, 9] is arithmetic with difference 2
  • [7, 7, 7, 7] is arithmetic with difference 0
  • [3, -1, -5, -9] is arithmetic with difference -4
  • [1, 1, 2, 5, 7] is NOT arithmetic (differences are 0, 1, 3, 2)

A subsequence is formed by selecting elements from the original array while maintaining their relative order. You can skip elements but cannot rearrange them.

For example, from array [1, 2, 1, 2, 4, 1, 5, 10]:

  • [2, 5, 10] is a valid subsequence (taking 2nd, 7th, and 8th elements)

The solution uses dynamic programming with a dictionary at each position. For each index i, f[i][d] stores the count of arithmetic subsequences ending at position i with common difference d.

The algorithm works by:

  1. For each position i and element x
  2. Looking at all previous positions j with element y
  3. Calculating the difference d = x - y
  4. Adding to the answer the number of subsequences ending at j with difference d (these can be extended by x to form valid arithmetic subsequences)
  5. Updating f[i][d] to include all subsequences that can be formed by extending those from position j, plus the new 2-element subsequence [y, x]

The final answer is the sum of all valid arithmetic subsequences found.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that we need to track arithmetic subsequences as we build them incrementally. When we're at position i, we want to know: what arithmetic subsequences can we form that end at this position?

Think about it this way: if we have an arithmetic subsequence ending at position j with difference d, and we find that nums[i] - nums[j] = d, then we can extend that subsequence by adding nums[i]. This creates a new arithmetic subsequence ending at position i.

But here's the crucial observation: we need to track subsequences of length 2 as well (even though they don't count as arithmetic subsequences yet) because they can become valid arithmetic subsequences when extended by a third element.

For example, if we have [1, 3] with difference 2, it's not yet an arithmetic subsequence. But if we later encounter 5, we can form [1, 3, 5] which IS valid.

This leads us to use dynamic programming where f[i][d] represents the number of arithmetic subsequences (including 2-element ones) ending at index i with common difference d.

When processing element at index i:

  • We look at all previous elements at index j
  • Calculate the difference d = nums[i] - nums[j]
  • Any subsequence ending at j with difference d can be extended to i
  • Those that were already length ≥2 become valid arithmetic subsequences when extended (so we add f[j][d] to our answer)
  • We also form a new 2-element subsequence [nums[j], nums[i]] (the "+1" in the update)

The beauty of this approach is that it naturally handles the "at least 3 elements" requirement: only when we extend a 2+ element subsequence do we count it in our answer, ensuring all counted subsequences have at least 3 elements.

Solution Approach

The solution uses dynamic programming with hash maps to efficiently track arithmetic subsequences.

Data Structure:

  • f: A list of dictionaries where f[i] is a dictionary storing counts of subsequences ending at index i
  • Each dictionary f[i] has keys as differences d and values as counts of subsequences with that difference

Algorithm Steps:

  1. Initialize: Create a list f where each element is an empty defaultdict(int). This allows us to store counts for different differences at each position.

  2. Iterate through each position i with element x:

    for i, x in enumerate(nums):
  3. For each position i, check all previous positions j with element y:

    for j, y in enumerate(nums[:i]):
  4. Calculate the difference between current and previous element:

    d = x - y
  5. Count valid arithmetic subsequences:

    ans += f[j][d]

    This is the key step - if there are f[j][d] subsequences ending at j with difference d, they can all be extended by adding element x to form valid arithmetic subsequences of length ≥3.

  6. Update the DP state:

    f[i][d] += f[j][d] + 1
    • f[j][d]: Represents all subsequences ending at j with difference d that can be extended to i
    • +1: Represents the new 2-element subsequence [nums[j], nums[i]]

Example Walkthrough:

For nums = [2, 4, 6, 8]:

  • At i=1 (value 4): Form [2,4] with difference 2
  • At i=2 (value 6):
    • Extend [2,4] to [2,4,6] (count this!)
    • Form [4,6] with difference 2
  • At i=3 (value 8):
    • Extend [2,4,6] to [2,4,6,8] (count this!)
    • Extend [4,6] to [4,6,8] (count this!)
    • Form [6,8] with difference 2

Time Complexity: O(n²) where n is the length of nums, as we have nested loops.

Space Complexity: O(n²) in worst case, as each position can store up to n different differences.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through nums = [1, 3, 5, 7] step by step to see how the algorithm counts arithmetic subsequences.

Initial Setup:

  • f = [{}, {}, {}, {}] (list of empty dictionaries)
  • ans = 0

Step 1: i=0, x=1

  • No previous elements to check
  • f[0] remains empty: {}

Step 2: i=1, x=3

  • Check j=0, y=1:
    • d = 3 - 1 = 2
    • ans += f[0][2] = 0 (no subsequences at position 0 with difference 2)
    • f[1][2] = f[0][2] + 1 = 0 + 1 = 1 (creates 2-element subsequence [1,3])
  • State: f[1] = {2: 1}, ans = 0

Step 3: i=2, x=5

  • Check j=0, y=1:
    • d = 5 - 1 = 4
    • ans += f[0][4] = 0
    • f[2][4] = 0 + 1 = 1 (creates [1,5])
  • Check j=1, y=3:
    • d = 5 - 3 = 2
    • ans += f[1][2] = 1 ✓ (extending [1,3] to [1,3,5] creates our first valid subsequence!)
    • f[2][2] = f[1][2] + 1 = 1 + 1 = 2 (one from extending [1,3], one new [3,5])
  • State: f[2] = {4: 1, 2: 2}, ans = 1

Step 4: i=3, x=7

  • Check j=0, y=1:
    • d = 7 - 1 = 6
    • ans += f[0][6] = 0
    • f[3][6] = 0 + 1 = 1 (creates [1,7])
  • Check j=1, y=3:
    • d = 7 - 3 = 4
    • ans += f[1][4] = 0
    • f[3][4] = 0 + 1 = 1 (creates [3,7])
  • Check j=2, y=5:
    • d = 7 - 5 = 2
    • ans += f[2][2] = 2 ✓ (two valid extensions: [1,3,5] → [1,3,5,7] and [3,5] → [3,5,7])
    • f[3][2] = f[2][2] + 1 = 2 + 1 = 3
  • State: f[3] = {6: 1, 4: 1, 2: 3}, ans = 3

Final Answer: 3

The three arithmetic subsequences found are:

  1. [1, 3, 5] (difference 2)
  2. [1, 3, 5, 7] (difference 2)
  3. [3, 5, 7] (difference 2)

The key insight: when we're at position i and find that nums[i] - nums[j] = d, the value f[j][d] tells us exactly how many subsequences ending at j can be extended to form valid arithmetic subsequences. This is why we add f[j][d] to our answer - each represents a newly formed arithmetic subsequence of length ≥3.

Solution Implementation

1from typing import List
2from collections import defaultdict
3
4class Solution:
5    def numberOfArithmeticSlices(self, nums: List[int]) -> int:
6        # dp[i][diff] = number of arithmetic subsequences ending at index i with difference diff
7        # These subsequences have at least 2 elements
8        dp = [defaultdict(int) for _ in nums]
9      
10        # Total count of arithmetic subsequences with at least 3 elements
11        total_count = 0
12      
13        # Iterate through each element as potential end of subsequence
14        for i, current_num in enumerate(nums):
15            # Check all previous elements as potential second-to-last element
16            for j, previous_num in enumerate(nums[:i]):
17                # Calculate the common difference
18                difference = current_num - previous_num
19              
20                # Add count of existing subsequences that can be extended
21                # dp[j][difference] represents subsequences ending at j with this difference
22                # Each of these becomes a valid arithmetic slice when extended with nums[i]
23                total_count += dp[j][difference]
24              
25                # Update dp for position i
26                # dp[j][difference] subsequences can be extended with current element
27                # Plus 1 for the new 2-element subsequence [nums[j], nums[i]]
28                dp[i][difference] += dp[j][difference] + 1
29      
30        return total_count
31
1class Solution {
2    public int numberOfArithmeticSlices(int[] nums) {
3        int n = nums.length;
4      
5        // dp[i] stores a map where key is the common difference and 
6        // value is the count of arithmetic subsequences ending at index i with that difference
7        Map<Long, Integer>[] dp = new Map[n];
8      
9        // Initialize each position with an empty HashMap
10        for (int i = 0; i < n; i++) {
11            dp[i] = new HashMap<>();
12        }
13      
14        int totalCount = 0;
15      
16        // For each element as the potential last element of arithmetic subsequence
17        for (int i = 0; i < n; i++) {
18            // Check all previous elements that could form a subsequence with current element
19            for (int j = 0; j < i; j++) {
20                // Calculate the common difference between current and previous element
21                // Using Long to avoid integer overflow
22                Long difference = (long) nums[i] - (long) nums[j];
23              
24                // Get count of arithmetic subsequences ending at j with this difference
25                // These can be extended by adding nums[i]
26                int previousCount = dp[j].getOrDefault(difference, 0);
27              
28                // Add all extendable subsequences to the result
29                // (subsequences of length >= 2 at j become length >= 3 when extended to i)
30                totalCount += previousCount;
31              
32                // Update dp[i]: add the extended subsequences plus the new pair (j, i)
33                // previousCount + 1 accounts for:
34                // - previousCount: extending existing subsequences from j
35                // - 1: the new two-element subsequence formed by (nums[j], nums[i])
36                dp[i].merge(difference, previousCount + 1, Integer::sum);
37            }
38        }
39      
40        return totalCount;
41    }
42}
43
1class Solution {
2public:
3    int numberOfArithmeticSlices(vector<int>& nums) {
4        int n = nums.size();
5      
6        // dp[i] stores a map where:
7        // - key: common difference
8        // - value: count of arithmetic subsequences ending at index i with that difference
9        unordered_map<long long, int> dp[n];
10      
11        int totalCount = 0;
12      
13        // Iterate through each element as the potential end of an arithmetic subsequence
14        for (int i = 0; i < n; ++i) {
15            // Check all previous elements as potential second-to-last elements
16            for (int j = 0; j < i; ++j) {
17                // Calculate the common difference between current and previous element
18                // Use long long to prevent integer overflow
19                long long diff = static_cast<long long>(nums[i]) - nums[j];
20              
21                // Get count of arithmetic subsequences ending at j with difference 'diff'
22                // This represents subsequences of length >= 2
23                int subsequenceCount = dp[j][diff];
24              
25                // Add to total count (these form valid arithmetic subsequences of length >= 3)
26                totalCount += subsequenceCount;
27              
28                // Update dp[i][diff]:
29                // - Add subsequenceCount: extend existing subsequences from j to i
30                // - Add 1: create new subsequence of length 2 (nums[j], nums[i])
31                dp[i][diff] += subsequenceCount + 1;
32            }
33        }
34      
35        return totalCount;
36    }
37};
38
1/**
2 * Counts the number of arithmetic subsequences of length >= 3 in the given array
3 * An arithmetic subsequence is a sequence where the difference between consecutive elements is constant
4 * 
5 * @param nums - The input array of numbers
6 * @returns The total count of arithmetic subsequences
7 */
8function numberOfArithmeticSlices(nums: number[]): number {
9    const arrayLength: number = nums.length;
10  
11    // dp[i] is a map where:
12    // - key: the common difference
13    // - value: count of arithmetic subsequences ending at index i with this difference
14    const dp: Map<number, number>[] = new Array(arrayLength)
15        .fill(0)
16        .map(() => new Map<number, number>());
17  
18    let totalCount: number = 0;
19  
20    // Iterate through each position as potential end of subsequence
21    for (let currentIndex = 0; currentIndex < arrayLength; ++currentIndex) {
22        // Check all previous elements as potential second-to-last element
23        for (let previousIndex = 0; previousIndex < currentIndex; ++previousIndex) {
24            // Calculate the common difference between current and previous element
25            const difference: number = nums[currentIndex] - nums[previousIndex];
26          
27            // Get count of subsequences ending at previousIndex with this difference
28            // These can be extended by adding current element
29            const previousSubsequenceCount: number = dp[previousIndex].get(difference) || 0;
30          
31            // Add all extendable subsequences to total
32            // (they become valid arithmetic subsequences of length >= 3)
33            totalCount += previousSubsequenceCount;
34          
35            // Update dp for current position:
36            // 1. Add all extended subsequences from previousIndex
37            // 2. Add 1 for new two-element subsequence [nums[previousIndex], nums[currentIndex]]
38            const currentCount: number = dp[currentIndex].get(difference) || 0;
39            dp[currentIndex].set(difference, currentCount + previousSubsequenceCount + 1);
40        }
41    }
42  
43    return totalCount;
44}
45

Time and Space Complexity

Time Complexity: O(n²)

The algorithm uses two nested loops:

  • The outer loop iterates through all elements in nums, running n times
  • The inner loop iterates through all elements before index i, which on average runs i/2 times
  • The total number of iterations is 1 + 2 + 3 + ... + (n-1) = n(n-1)/2
  • Inside the nested loops, all operations (dictionary access, arithmetic operations, and updates) take O(1) time
  • Therefore, the overall time complexity is O(n²)

Space Complexity: O(n²)

The space usage comes from:

  • The list f contains n dictionaries, one for each element in nums
  • Each dictionary f[i] can potentially store up to i different arithmetic differences (one for each previous element)
  • In the worst case where all differences are unique, the total space used is 0 + 1 + 2 + ... + (n-1) = n(n-1)/2
  • This results in O(n²) space complexity for storing all the difference counts
  • Additional space usage includes the loop variables and temporary variables, which are O(1)
  • Therefore, the overall space complexity is O(n²)

Common Pitfalls

1. Confusing Subsequences with Subarrays

A common mistake is treating this problem as counting arithmetic subarrays (contiguous elements) rather than subsequences (non-contiguous elements that maintain order).

Wrong Approach:

# This only counts contiguous arithmetic sequences
def countArithmeticSubarrays(nums):
    count = 0
    for i in range(len(nums) - 2):
        diff = nums[i+1] - nums[i]
        for j in range(i+2, len(nums)):
            if nums[j] - nums[j-1] == diff:
                count += 1
            else:
                break
    return count

Solution: Remember that subsequences can skip elements. The DP approach correctly handles this by considering all previous positions, not just adjacent ones.

2. Integer Overflow with Large Differences

When dealing with large numbers in the array, the difference d = x - y might exceed integer limits in some languages.

Problematic Example:

nums = [2147483647, -2147483648, 0]  # Max and min integers
# The difference could overflow in languages with fixed integer sizes

Solution: In Python, this isn't an issue due to arbitrary precision integers. In other languages, use appropriate data types (e.g., long long in C++) or check for overflow conditions.

3. Forgetting to Count Only Length ≥3 Subsequences

The DP state f[i][d] includes 2-element subsequences, but we only want to count subsequences with at least 3 elements.

Wrong Implementation:

def numberOfArithmeticSlices(nums):
    dp = [defaultdict(int) for _ in nums]
    total = 0
  
    for i in range(len(nums)):
        for j in range(i):
            d = nums[i] - nums[j]
            dp[i][d] += dp[j][d] + 1
            total += dp[i][d]  # WRONG: This counts 2-element subsequences too
  
    return total

Solution: Only add dp[j][d] to the answer, not the full dp[i][d], because dp[j][d] represents subsequences that already have at least 2 elements and will have at least 3 when extended.

4. Memory Issues with Dense Arrays

For arrays with many unique differences, the space complexity can become problematic.

Problematic Case:

nums = [1, 2, 4, 8, 16, 32, ...]  # Many unique differences
# Each pair creates a unique difference, leading to O(n²) space usage

Solution: While the algorithm is correct, be aware of memory constraints. For very large inputs with many unique differences, consider:

  • Using regular dictionaries instead of defaultdict to save some overhead
  • Clearing dictionary entries with value 0 to save space
  • Implementing early termination if memory becomes critical

5. Incorrect Loop Boundary

Using enumerate(nums[:i]) creates a new list slice each iteration, which is inefficient.

Better Implementation:

for i in range(len(nums)):
    for j in range(i):  # More efficient than enumerate(nums[:i])
        d = nums[i] - nums[j]
        total_count += dp[j][d]
        dp[i][d] += dp[j][d] + 1

This avoids creating unnecessary list slices and improves performance.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following is a good use case for backtracking?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More