1218. Longest Arithmetic Subsequence of Given Difference
Problem Description
You are given an integer array arr
and an integer difference
. Your task is to find the length of the longest subsequence in arr
that forms an arithmetic sequence where the difference between any two adjacent elements in the subsequence equals difference
.
A subsequence is a sequence that can be obtained from the original array by deleting some elements (or none) without changing the order of the remaining elements. The elements in the subsequence don't need to be consecutive in the original array.
For example, if arr = [1, 2, 3, 4]
and difference = 1
, the entire array forms an arithmetic subsequence with difference 1, so the answer would be 4. If arr = [1, 5, 7, 8, 5, 3, 4, 2, 1]
and difference = -2
, the longest arithmetic subsequence would be [7, 5, 3, 1]
with length 4.
The solution uses dynamic programming with a hash table approach. For each element x
in the array, it calculates the length of the longest arithmetic subsequence ending at x
by looking up the length of the subsequence ending at x - difference
and adding 1. The hash table f
stores these lengths, where f[x]
represents the length of the longest arithmetic subsequence ending with value x
. The final answer is the maximum value among all lengths stored in the hash table.
Intuition
When we think about building an arithmetic subsequence, we need to make decisions about which elements to include. The key insight is that for any element x
in the array, if we want to include it in an arithmetic subsequence with a given difference, the previous element in that subsequence must be x - difference
.
This observation leads us to a dynamic programming approach. As we traverse the array from left to right, for each element x
, we ask: "What's the longest arithmetic subsequence that ends with x
?"
To answer this, we need to know if there's an arithmetic subsequence ending with x - difference
that we can extend. If such a subsequence exists with length k
, then by adding x
to it, we create a subsequence of length k + 1
. If no such subsequence exists, then x
starts a new subsequence of length 1.
The beauty of this approach is that we don't need to track all possible subsequences explicitly. We only need to remember, for each value we've seen, the length of the longest arithmetic subsequence ending with that value. This is why a hash table is perfect - it allows us to quickly look up f[x - difference]
to determine how to update f[x]
.
As we process each element, we're essentially building up all possible arithmetic subsequences simultaneously, keeping track of only the longest one ending at each value. The final answer is simply the maximum length among all these subsequences.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution implements a dynamic programming approach using a hash table to efficiently track the longest arithmetic subsequence ending at each value.
Data Structure Used:
- A hash table
f
(implemented asdefaultdict(int)
) where the key is a value from the array and the value is the length of the longest arithmetic subsequence ending with that key.
Algorithm Steps:
-
Initialize the hash table: Create a
defaultdict(int)
which automatically returns 0 for non-existent keys. This handles the base case where an element starts a new subsequence. -
Iterate through the array: Process each element
x
inarr
sequentially from left to right. -
Update the DP state: For each element
x
, compute the length of the longest arithmetic subsequence ending atx
using the recurrence relation:f[x] = f[x - difference] + 1
- If
x - difference
exists in our hash table, it means we can extend that subsequence by includingx
- If
x - difference
doesn't exist,f[x - difference]
returns 0, sof[x]
becomes 1 (starting a new subsequence)
- If
-
Find the maximum: After processing all elements, return
max(f.values())
which gives us the length of the longest arithmetic subsequence in the entire array.
Time Complexity: O(n)
where n
is the length of the array, as we process each element once and hash table operations are O(1)
on average.
Space Complexity: O(n)
in the worst case when all elements are unique, as we might store up to n
entries in the hash table.
The elegance of this solution lies in its simplicity - by maintaining just the length of the best subsequence ending at each value, we avoid the complexity of tracking the actual subsequences themselves.
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 a concrete example with arr = [1, 5, 7, 8, 5, 3, 4, 2, 1]
and difference = -2
.
We'll track how our hash table f
evolves as we process each element:
Initial state: f = {}
(empty hash table)
Step 1: Process x = 1
- Check
f[1 - (-2)] = f[3]
→ not found, returns 0 - Update:
f[1] = 0 + 1 = 1
- State:
f = {1: 1}
Step 2: Process x = 5
- Check
f[5 - (-2)] = f[7]
→ not found, returns 0 - Update:
f[5] = 0 + 1 = 1
- State:
f = {1: 1, 5: 1}
Step 3: Process x = 7
- Check
f[7 - (-2)] = f[9]
→ not found, returns 0 - Update:
f[7] = 0 + 1 = 1
- State:
f = {1: 1, 5: 1, 7: 1}
Step 4: Process x = 8
- Check
f[8 - (-2)] = f[10]
→ not found, returns 0 - Update:
f[8] = 0 + 1 = 1
- State:
f = {1: 1, 5: 1, 7: 1, 8: 1}
Step 5: Process x = 5
(second occurrence)
- Check
f[5 - (-2)] = f[7]
→ found! Returns 1 - Update:
f[5] = 1 + 1 = 2
(we can extend the subsequence ending at 7) - State:
f = {1: 1, 5: 2, 7: 1, 8: 1}
- This represents the subsequence
[7, 5]
Step 6: Process x = 3
- Check
f[3 - (-2)] = f[5]
→ found! Returns 2 - Update:
f[3] = 2 + 1 = 3
(extending[7, 5]
to[7, 5, 3]
) - State:
f = {1: 1, 5: 2, 7: 1, 8: 1, 3: 3}
Step 7: Process x = 4
- Check
f[4 - (-2)] = f[6]
→ not found, returns 0 - Update:
f[4] = 0 + 1 = 1
- State:
f = {1: 1, 5: 2, 7: 1, 8: 1, 3: 3, 4: 1}
Step 8: Process x = 2
- Check
f[2 - (-2)] = f[4]
→ found! Returns 1 - Update:
f[2] = 1 + 1 = 2
- State:
f = {1: 1, 5: 2, 7: 1, 8: 1, 3: 3, 4: 1, 2: 2}
Step 9: Process x = 1
(second occurrence)
- Check
f[1 - (-2)] = f[3]
→ found! Returns 3 - Update:
f[1] = 3 + 1 = 4
(extending[7, 5, 3]
to[7, 5, 3, 1]
) - Final state:
f = {1: 4, 5: 2, 7: 1, 8: 1, 3: 3, 4: 1, 2: 2}
Result: max(f.values()) = 4
The longest arithmetic subsequence is [7, 5, 3, 1]
with length 4, where each consecutive pair has a difference of -2.
Solution Implementation
1from collections import defaultdict
2from typing import List
3
4class Solution:
5 def longestSubsequence(self, arr: List[int], difference: int) -> int:
6 # Dictionary to store the length of longest arithmetic subsequence ending at each value
7 # Key: array element value, Value: length of longest subsequence ending at this value
8 dp = defaultdict(int)
9
10 # Iterate through each element in the array
11 for num in arr:
12 # For current number, the longest subsequence ending at it equals:
13 # 1 + length of longest subsequence ending at (num - difference)
14 # This works because we need consecutive elements to differ by 'difference'
15 dp[num] = dp[num - difference] + 1
16
17 # Return the maximum length among all subsequences
18 return max(dp.values())
19
1class Solution {
2 public int longestSubsequence(int[] arr, int difference) {
3 // HashMap to store the length of arithmetic subsequence ending at each value
4 // Key: the ending value of subsequence
5 // Value: the maximum length of arithmetic subsequence ending at this value
6 Map<Integer, Integer> dp = new HashMap<>();
7
8 // Variable to track the maximum length found so far
9 int maxLength = 0;
10
11 // Iterate through each element in the array
12 for (int currentValue : arr) {
13 // For current value, check if (currentValue - difference) exists in the map
14 // If it exists, we can extend that subsequence by adding current value
15 // If not, start a new subsequence with length 1
16 int previousLength = dp.getOrDefault(currentValue - difference, 0);
17 int currentLength = previousLength + 1;
18
19 // Update the maximum length of subsequence ending at currentValue
20 dp.put(currentValue, currentLength);
21
22 // Update the global maximum length
23 maxLength = Math.max(maxLength, currentLength);
24 }
25
26 return maxLength;
27 }
28}
29
1class Solution {
2public:
3 int longestSubsequence(vector<int>& arr, int difference) {
4 // HashMap to store the length of longest arithmetic subsequence ending at each value
5 // Key: the ending value, Value: length of subsequence ending at that value
6 unordered_map<int, int> dp;
7
8 // Track the maximum length found so far
9 int maxLength = 0;
10
11 // Iterate through each element in the array
12 for (int currentValue : arr) {
13 // For current value, the longest subsequence ending here is:
14 // 1 + length of subsequence ending at (currentValue - difference)
15 // If (currentValue - difference) doesn't exist, dp[currentValue - difference] returns 0
16 dp[currentValue] = dp[currentValue - difference] + 1;
17
18 // Update the maximum length if current subsequence is longer
19 maxLength = max(maxLength, dp[currentValue]);
20 }
21
22 // Return the length of the longest arithmetic subsequence
23 return maxLength;
24 }
25};
26
1/**
2 * Finds the length of the longest arithmetic subsequence with given difference
3 * @param arr - The input array of numbers
4 * @param difference - The common difference for the arithmetic subsequence
5 * @returns The length of the longest arithmetic subsequence
6 */
7function longestSubsequence(arr: number[], difference: number): number {
8 // Map to store the length of longest subsequence ending at each number
9 const subsequenceLengthMap: Map<number, number> = new Map();
10
11 // Iterate through each number in the array
12 for (const currentNumber of arr) {
13 // Calculate the previous number in the arithmetic sequence
14 const previousNumber = currentNumber - difference;
15
16 // Get the length of subsequence ending at previousNumber (0 if not exists)
17 const previousLength = subsequenceLengthMap.get(previousNumber) ?? 0;
18
19 // Update the length of subsequence ending at currentNumber
20 // It's either extending the previous subsequence or starting a new one
21 subsequenceLengthMap.set(currentNumber, previousLength + 1);
22 }
23
24 // Return the maximum length among all subsequences
25 return Math.max(...subsequenceLengthMap.values());
26}
27
Time and Space Complexity
Time Complexity: O(n)
- The algorithm iterates through the array
arr
exactly once with a single for loop - For each element
x
in the array, it performs constant time operations:- Looking up
f[x - difference]
in the dictionary:O(1)
average case - Updating
f[x]
in the dictionary:O(1)
average case
- Looking up
- After the loop,
max(f.values())
runs inO(n)
time in the worst case when all elements form a valid subsequence - Total time complexity:
O(n) + O(n) = O(n)
, wheren
is the length of the arrayarr
Space Complexity: O(n)
- The dictionary
f
stores at mostn
key-value pairs (one for each unique element in the array) - In the worst case, when all elements are distinct, the dictionary will contain
n
entries - Therefore, the space complexity is
O(n)
, wheren
is the length of the arrayarr
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Handling Duplicate Values Incorrectly
The Problem: A critical pitfall in this solution is how duplicate values are handled. When the same value appears multiple times in the array, the hash table will overwrite the previous entry, potentially leading to incorrect results.
Consider this example:
arr = [1, 3, 5, 3, 7]
,difference = 2
- When we process the first
3
, we getdp[3] = dp[1] + 1 = 2
(sequence: [1, 3]) - When we process
5
, we getdp[5] = dp[3] + 1 = 3
(sequence: [1, 3, 5]) - When we process the second
3
, we updatedp[3] = dp[1] + 1 = 2
again - When we process
7
, we getdp[7] = dp[5] + 1 = 4
(correctly using [1, 3, 5, 7])
In this case, the algorithm works correctly because we process elements left to right, and the hash table always maintains the most recent occurrence's subsequence length. However, this behavior might not be immediately obvious and could cause confusion.
Why This Actually Works: The algorithm is correct because:
- We process elements from left to right
- For any arithmetic subsequence, we only care about extending from the most recent valid predecessor
- Overwriting with a duplicate doesn't affect future calculations since we've already processed all elements that could have used the previous value
Pitfall 2: Integer Overflow in Different Languages
The Problem:
When calculating num - difference
, integer overflow could occur in languages with fixed integer sizes. For example:
- If
num = -2^31
(minimum 32-bit integer) anddifference = 1
- Then
num - difference = -2^31 - 1
would cause underflow
Solution: In Python, this isn't an issue due to arbitrary precision integers, but when implementing in other languages:
# For languages with fixed integer sizes, consider bounds checking:
def longestSubsequence(self, arr: List[int], difference: int) -> int:
dp = defaultdict(int)
for num in arr:
# Check if num - difference would cause overflow/underflow
prev = num - difference
# In Python this check is unnecessary, but in C++/Java you might need:
# if (difference > 0 && num < INT_MIN + difference) continue;
# if (difference < 0 && num > INT_MAX + difference) continue;
dp[num] = dp[prev] + 1
return max(dp.values())
Pitfall 3: Empty Array Edge Case
The Problem:
If the input array is empty, max(dp.values())
will raise a ValueError
because you cannot take the max of an empty sequence.
Solution: Add a guard clause to handle empty arrays:
def longestSubsequence(self, arr: List[int], difference: int) -> int:
if not arr: # Handle empty array
return 0
dp = defaultdict(int)
for num in arr:
dp[num] = dp[num - difference] + 1
return max(dp.values())
Pitfall 4: Misunderstanding the Problem Requirements
The Problem: Developers might mistakenly think they need to track the actual subsequence elements or that the subsequence elements must be consecutive in the original array. This could lead to overly complex solutions.
Clarification:
- We only need the length, not the actual subsequence
- Elements in the subsequence maintain their relative order but don't need to be consecutive in the original array
- The arithmetic property only applies to the subsequence, not the original array positions
What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?
Recommended Readings
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
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!