Facebook Pixel

1027. Longest Arithmetic Subsequence

Problem Description

You are given an array nums of integers. Your task is to find the length of the longest arithmetic subsequence that can be formed from this array.

An arithmetic subsequence is a sequence where the difference between consecutive elements is constant. For example, [3, 7, 11, 15] is arithmetic because each pair of consecutive elements has a difference of 4.

A subsequence is derived from the original array by deleting some or no elements while maintaining the relative order of the remaining elements. For instance, from array [1, 3, 5, 7, 9], we can form subsequence [1, 5, 9] by removing elements at positions 1 and 3.

The solution uses dynamic programming with a 2D array f[i][j] where:

  • i represents the index of the current element in nums
  • j represents the common difference (offset by 500 to handle negative differences)
  • f[i][j] stores the maximum length of an arithmetic subsequence ending at nums[i] with common difference j - 500

The algorithm works by:

  1. For each element nums[i], examining all previous elements nums[k] where k < i
  2. Calculating the difference j = nums[i] - nums[k] + 500 (adding 500 to handle negative differences since array indices must be non-negative)
  3. Updating f[i][j] to be the maximum of its current value or f[k][j] + 1 (extending the arithmetic sequence ending at nums[k])
  4. Tracking the maximum length found across all subsequences

The range of 1001 for the second dimension accounts for differences ranging from -500 to 500, shifted to [0, 1000] for array indexing purposes.

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

Intuition

The key insight is that an arithmetic subsequence is uniquely defined by two properties: its ending element and its common difference. If we know these two pieces of information, we can determine the entire subsequence by working backwards.

Think about building arithmetic subsequences incrementally. When we're at position i in the array, we want to know: what's the longest arithmetic subsequence that ends at nums[i]? But this question is incomplete - we also need to specify the common difference, because nums[i] could be the end of multiple arithmetic subsequences with different common differences.

For example, if nums = [1, 3, 5, 7] and we're at element 7, it could be:

  • Part of subsequence [1, 7] with difference 6
  • Part of subsequence [3, 5, 7] with difference 2
  • Part of subsequence [5, 7] with difference 2

This naturally leads to a dynamic programming approach where our state is (ending position, common difference). For each element nums[i], we consider pairing it with every previous element nums[k] to form a potential arithmetic sequence. The difference nums[i] - nums[k] becomes our common difference.

The beautiful part is that if nums[k] was already part of an arithmetic subsequence with the same common difference, we can simply extend that subsequence by adding nums[i]. This is why f[i][j] = f[k][j] + 1 - we're taking the longest arithmetic subsequence ending at nums[k] with difference j and extending it by one element.

The offset of 500 is a technical detail to handle negative differences. Since array indices must be non-negative, and differences can range from -500 to 500 (based on typical constraints), we shift everything by 500 to map the range to [0, 1000].

Learn more about Binary Search and Dynamic Programming patterns.

Solution Approach

We implement the solution using dynamic programming with a 2D table. The approach follows these steps:

1. Initialize the DP Table

Create a 2D array f where f[i][j] represents the maximum length of an arithmetic subsequence ending at index i with common difference j - 500. Initialize all values to 1 since each element by itself forms an arithmetic subsequence of length 1.

f = [[1] * 1001 for _ in range(n)]

The size 1001 accounts for all possible differences in the range [-500, 500] shifted to [0, 1000].

2. Fill the DP Table

For each position i from 1 to n-1:

  • Consider all previous positions k from 0 to i-1
  • Calculate the common difference: j = nums[i] - nums[k] + 500
  • Update the DP value: f[i][j] = max(f[i][j], f[k][j] + 1)

This means we're either:

  • Starting a new arithmetic subsequence of length 2 (since f[i][j] was initialized to 1)
  • Extending an existing arithmetic subsequence that ended at position k with the same common difference
for i in range(1, n):
    for k in range(i):
        j = nums[i] - nums[k] + 500
        f[i][j] = max(f[i][j], f[k][j] + 1)
        ans = max(ans, f[i][j])

3. Track the Maximum Length

Throughout the process, we maintain ans as the maximum length found so far. We update it whenever we compute a new f[i][j] value.

Time and Space Complexity

  • Time Complexity: O(n² × d) where n is the length of the array and d is the range of differences. Since d is constant (1001), this simplifies to O(n²).
  • Space Complexity: O(n × d) which is O(n) since d is constant.

The algorithm efficiently finds the longest arithmetic subsequence by building up solutions for smaller subsequences and using them to construct longer ones, avoiding redundant calculations through memoization.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's walk through the algorithm with nums = [3, 6, 9, 12].

Step 1: Initialize

  • Create DP table f with dimensions 4×1001 (4 elements, 1001 possible differences)
  • Initialize all values to 1
  • Set ans = 1

Step 2: Process each element

When i = 1 (nums[1] = 6):

  • Consider k = 0 (nums[0] = 3):
    • Difference = 6 - 3 = 3, so j = 3 + 500 = 503
    • f[1][503] = max(1, f[0][503] + 1) = max(1, 1 + 1) = 2
    • Update ans = 2

When i = 2 (nums[2] = 9):

  • Consider k = 0 (nums[0] = 3):
    • Difference = 9 - 3 = 6, so j = 506
    • f[2][506] = max(1, f[0][506] + 1) = 2
    • ans remains 2
  • Consider k = 1 (nums[1] = 6):
    • Difference = 9 - 6 = 3, so j = 503
    • f[2][503] = max(1, f[1][503] + 1) = max(1, 2 + 1) = 3
    • Update ans = 3

When i = 3 (nums[3] = 12):

  • Consider k = 0 (nums[0] = 3):
    • Difference = 12 - 3 = 9, so j = 509
    • f[3][509] = 2
    • ans remains 3
  • Consider k = 1 (nums[1] = 6):
    • Difference = 12 - 6 = 6, so j = 506
    • f[3][506] = max(1, f[1][506] + 1) = 2
    • ans remains 3
  • Consider k = 2 (nums[2] = 9):
    • Difference = 12 - 9 = 3, so j = 503
    • f[3][503] = max(1, f[2][503] + 1) = max(1, 3 + 1) = 4
    • Update ans = 4

Result: The longest arithmetic subsequence is [3, 6, 9, 12] with length 4.

The key insight is how f[2][503] = 3 represents the subsequence [3, 6, 9] with difference 3, which we then extend to include 12, giving us f[3][503] = 4 for the complete sequence [3, 6, 9, 12].

Solution Implementation

1class Solution:
2    def longestArithSeqLength(self, nums: List[int]) -> int:
3        """
4        Find the length of the longest arithmetic subsequence in the given array.
5      
6        An arithmetic subsequence is a sequence where the difference between 
7        consecutive elements is constant.
8      
9        Args:
10            nums: List of integers
11          
12        Returns:
13            Length of the longest arithmetic subsequence
14        """
15        n = len(nums)
16      
17        # dp[i][diff] represents the length of the longest arithmetic subsequence
18        # ending at index i with common difference 'diff'
19        # Since difference can range from -500 to 500 (based on constraints),
20        # we offset by 500 to avoid negative indices (0 to 1000)
21        dp = [[1] * 1001 for _ in range(n)]
22      
23        max_length = 0
24      
25        # For each position i, check all previous positions
26        for i in range(1, n):
27            for prev_idx in range(i):
28                # Calculate the difference between current and previous element
29                # Add 500 offset to handle negative differences as array indices
30                diff_with_offset = nums[i] - nums[prev_idx] + 500
31              
32                # Extend the arithmetic subsequence ending at prev_idx
33                # with the same difference to include nums[i]
34                dp[i][diff_with_offset] = max(
35                    dp[i][diff_with_offset], 
36                    dp[prev_idx][diff_with_offset] + 1
37                )
38              
39                # Update the maximum length found so far
40                max_length = max(max_length, dp[i][diff_with_offset])
41      
42        return max_length
43
1class Solution {
2    public int longestArithSeqLength(int[] nums) {
3        int n = nums.length;
4        int maxLength = 0;
5      
6        // dp[i][diff] represents the length of the longest arithmetic sequence 
7        // ending at index i with difference diff
8        // Since difference can be negative (range: -500 to 500), we add 500 as offset
9        // to make all indices non-negative (0 to 1000)
10        int[][] dp = new int[n][1001];
11      
12        // Iterate through each element as the potential end of an arithmetic sequence
13        for (int i = 1; i < n; i++) {
14            // Check all previous elements as potential second-to-last element
15            for (int k = 0; k < i; k++) {
16                // Calculate the difference and apply offset to handle negative values
17                int difference = nums[i] - nums[k] + 500;
18              
19                // Update dp[i][difference]: either extend the sequence from k 
20                // or start a new sequence of length 2 (represented as 1 here)
21                dp[i][difference] = Math.max(dp[i][difference], dp[k][difference] + 1);
22              
23                // Track the maximum length found so far
24                maxLength = Math.max(maxLength, dp[i][difference]);
25            }
26        }
27      
28        // Add 1 because dp stores count of transitions, not actual sequence length
29        // A sequence with n elements has n-1 transitions
30        return maxLength + 1;
31    }
32}
33
1class Solution {
2public:
3    int longestArithSeqLength(vector<int>& nums) {
4        int n = nums.size();
5      
6        // dp[i][diff] represents the length of arithmetic sequence ending at index i 
7        // with difference 'diff' (offset by 500 to handle negative differences)
8        // Since difference range is [-500, 500], we use index [0, 1000] after offset
9        int dp[n][1001];
10        memset(dp, 0, sizeof(dp));
11      
12        int maxLength = 0;
13      
14        // Iterate through each element as the ending point of arithmetic sequence
15        for (int i = 1; i < n; ++i) {
16            // Check all previous elements as potential previous element in sequence
17            for (int prev = 0; prev < i; ++prev) {
18                // Calculate the difference and offset by 500 to handle negative values
19                // This maps difference range [-500, 500] to array index [0, 1000]
20                int diff = nums[i] - nums[prev] + 500;
21              
22                // Extend the arithmetic sequence ending at prev with same difference
23                // or start a new sequence of length 2 (implied by +1 in return statement)
24                dp[i][diff] = max(dp[i][diff], dp[prev][diff] + 1);
25              
26                // Update the maximum length found so far
27                maxLength = max(maxLength, dp[i][diff]);
28            }
29        }
30      
31        // Add 1 because dp stores the count of transitions, not elements
32        // A sequence with 0 transitions has 1 element, 1 transition has 2 elements, etc.
33        return maxLength + 1;
34    }
35};
36
1/**
2 * Finds the length of the longest arithmetic subsequence in the given array
3 * @param nums - Input array of numbers
4 * @returns The length of the longest arithmetic subsequence
5 */
6function longestArithSeqLength(nums: number[]): number {
7    const arrayLength: number = nums.length;
8    let maxSequenceLength: number = 0;
9  
10    // Create a 2D DP array where dp[i][diff] represents the length of arithmetic sequence
11    // ending at index i with difference 'diff'
12    // Since difference can range from -500 to 500, we offset by 500 to use as array index
13    const dp: number[][] = Array.from(
14        { length: arrayLength }, 
15        () => new Array(1001).fill(0)
16    );
17  
18    // Iterate through each position as potential end of arithmetic sequence
19    for (let currentIndex = 1; currentIndex < arrayLength; ++currentIndex) {
20        // Check all previous elements as potential second-to-last element in sequence
21        for (let previousIndex = 0; previousIndex < currentIndex; ++previousIndex) {
22            // Calculate the difference and offset it to use as array index (range: 0-1000)
23            const differenceIndex: number = nums[currentIndex] - nums[previousIndex] + 500;
24          
25            // Update DP: extend the sequence ending at previousIndex with same difference
26            // or start a new sequence of length 2
27            dp[currentIndex][differenceIndex] = Math.max(
28                dp[currentIndex][differenceIndex], 
29                dp[previousIndex][differenceIndex] + 1
30            );
31          
32            // Track the maximum sequence length found so far
33            maxSequenceLength = Math.max(maxSequenceLength, dp[currentIndex][differenceIndex]);
34        }
35    }
36  
37    // Add 1 because dp stores the number of transitions, not the sequence length
38    // A sequence with n transitions has n+1 elements
39    return maxSequenceLength + 1;
40}
41

Time and Space Complexity

The time complexity is O(n²), where n is the length of the array nums.

The space complexity is O(n × d), where n is the length of the array nums and d is the range of possible differences.

Analysis:

For time complexity:

  • The outer loop runs n-1 times (from index 1 to n-1)
  • The inner loop runs up to i times for each iteration of the outer loop
  • This gives us a total of 1 + 2 + 3 + ... + (n-1) = n(n-1)/2 iterations
  • Therefore, the time complexity is O(n²)

For space complexity:

  • We create a 2D array f with dimensions n × 1001
  • The value 1001 represents the range of possible differences after shifting by 500 (to handle negative differences)
  • Since the array stores differences in the range [-500, 500], which is shifted to [0, 1000], we need 1001 positions
  • This range d = 1001 is constant in this implementation but conceptually represents the difference between maximum and minimum possible values
  • Therefore, the space complexity is O(n × 1001) = O(n × d)

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Handling of Negative Differences

Pitfall: A common mistake is forgetting to offset the difference when using it as an array index. Since Python (and most languages) don't allow negative array indices for standard arrays, directly using nums[i] - nums[prev_idx] as an index will cause an error when the difference is negative.

Incorrect Code:

diff = nums[i] - nums[prev_idx]  # Could be negative!
dp[i][diff] = max(dp[i][diff], dp[prev_idx][diff] + 1)  # IndexError!

Solution: Always add an offset (typically 500 based on constraints) to ensure all differences map to valid non-negative indices:

diff_with_offset = nums[i] - nums[prev_idx] + 500
dp[i][diff_with_offset] = max(dp[i][diff_with_offset], dp[prev_idx][diff_with_offset] + 1)

2. Insufficient Array Size for DP Table

Pitfall: Using an incorrect size for the second dimension of the DP table. If the problem constraints indicate values can range from -500 to 500, the difference between any two elements can range from -1000 to 1000.

Incorrect Code:

dp = [[1] * 1001 for _ in range(n)]  # May not cover all possible differences!

Solution: Calculate the actual range based on problem constraints. If elements range from -500 to 500, differences range from -1000 to 1000, requiring an array size of 2001:

# If nums[i] can be [-500, 500], differences can be [-1000, 1000]
dp = [[1] * 2001 for _ in range(n)]
diff_with_offset = nums[i] - nums[prev_idx] + 1000  # Offset by 1000

3. Forgetting to Initialize Base Cases

Pitfall: Not initializing the DP table properly. Each element by itself forms an arithmetic subsequence of length 1, but forgetting this initialization leads to incorrect results.

Incorrect Code:

dp = [[0] * 1001 for _ in range(n)]  # Wrong! Should be 1, not 0

Solution: Initialize all values to 1 since every single element is an arithmetic subsequence of length 1:

dp = [[1] * 1001 for _ in range(n)]

4. Not Tracking the Maximum Properly

Pitfall: Only checking the maximum at the last position or forgetting to update it during the iteration.

Incorrect Code:

for i in range(1, n):
    for prev_idx in range(i):
        diff_with_offset = nums[i] - nums[prev_idx] + 500
        dp[i][diff_with_offset] = max(dp[i][diff_with_offset], 
                                      dp[prev_idx][diff_with_offset] + 1)
return max(max(row) for row in dp)  # Inefficient and unnecessary

Solution: Track the maximum length as you compute it:

max_length = 0
for i in range(1, n):
    for prev_idx in range(i):
        diff_with_offset = nums[i] - nums[prev_idx] + 500
        dp[i][diff_with_offset] = max(dp[i][diff_with_offset], 
                                      dp[prev_idx][diff_with_offset] + 1)
        max_length = max(max_length, dp[i][diff_with_offset])
return max_length

5. Memory Optimization Oversight

Pitfall: For very large inputs, the 2D DP table might consume excessive memory. Using a full 2D array when a dictionary-based approach could be more memory-efficient.

Memory-Optimized Solution:

def longestArithSeqLength(self, nums: List[int]) -> int:
    n = len(nums)
    dp = [{} for _ in range(n)]  # Use dictionaries instead of arrays
    max_length = 0
  
    for i in range(1, n):
        for prev_idx in range(i):
            diff = nums[i] - nums[prev_idx]
            # No offset needed with dictionaries
            dp[i][diff] = dp[prev_idx].get(diff, 1) + 1
            max_length = max(max_length, dp[i][diff])
  
    return max_length

This approach only stores the differences that actually occur, potentially saving significant memory when the range of differences is large but sparse.

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

Which two pointer techniques do you use to check if a string is a palindrome?


Recommended Readings

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

Load More