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 innums
j
represents the common difference (offset by 500 to handle negative differences)f[i][j]
stores the maximum length of an arithmetic subsequence ending atnums[i]
with common differencej - 500
The algorithm works by:
- For each element
nums[i]
, examining all previous elementsnums[k]
wherek < i
- Calculating the difference
j = nums[i] - nums[k] + 500
(adding 500 to handle negative differences since array indices must be non-negative) - Updating
f[i][j]
to be the maximum of its current value orf[k][j] + 1
(extending the arithmetic sequence ending atnums[k]
) - 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.
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 difference6
- Part of subsequence
[3, 5, 7]
with difference2
- Part of subsequence
[5, 7]
with difference2
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
from0
toi-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)
wheren
is the length of the array andd
is the range of differences. Sinced
is constant (1001), this simplifies toO(n²)
. - Space Complexity:
O(n × d)
which isO(n)
sinced
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 EvaluatorExample 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 dimensionsn × 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.
Which two pointer techniques do you use to check if a string is a palindrome?
Recommended Readings
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!