Taro Logo

Longest Subsequence With Decreasing Adjacent Difference

Medium
Asked by:
Profile picture
33 views
Topics:
ArraysDynamic Programming

You are given an array of integers nums.

Your task is to find the length of the longest subsequence seq of nums, such that the absolute differences between consecutive elements form a non-increasing sequence of integers. In other words, for a subsequence seq0, seq1, seq2, ..., seqm of nums, |seq1 - seq0| >= |seq2 - seq1| >= ... >= |seqm - seqm - 1|.

Return the length of such a subsequence.

Example 1:

Input: nums = [16,6,3]

Output: 3

Explanation: 

The longest subsequence is [16, 6, 3] with the absolute adjacent differences [10, 3].

Example 2:

Input: nums = [6,5,3,4,2,1]

Output: 4

Explanation:

The longest subsequence is [6, 4, 2, 1] with the absolute adjacent differences [2, 2, 1].

Example 3:

Input: nums = [10,20,10,19,10,20]

Output: 5

Explanation: 

The longest subsequence is [10, 20, 10, 19, 10] with the absolute adjacent differences [10, 10, 9, 9].

Constraints:

  • 2 <= nums.length <= 104
  • 1 <= nums[i] <= 300

Solution


Clarifying Questions

When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:

  1. What is the range of values within the input array, and can it contain negative numbers, zeros, or floating-point numbers?
  2. What should I return if the input array is null or empty? Should I return an empty list/array or throw an exception?
  3. Are duplicate values allowed in the input array, and if so, how should they be handled when constructing the decreasing adjacent difference subsequence?
  4. By "longest subsequence," do you want the subsequence with the maximum number of elements, or is there a secondary criterion for determining 'longest' if multiple subsequences have the same length?
  5. Should I return the actual subsequence itself, or just the length of the longest subsequence?

Brute Force Solution

Approach

The brute force method for this problem involves checking every possible subsequence within the given sequence. We will generate all possible subsequences and then examine each one to see if it meets the decreasing adjacent difference condition. The longest valid subsequence found during this exhaustive search will be our answer.

Here's how the algorithm would work step-by-step:

  1. Consider all possible groups of numbers that can be formed from the given sequence.
  2. For each of these groups, check if the difference between adjacent numbers consistently decreases.
  3. Keep track of all the groups where the difference between adjacent numbers does consistently decrease.
  4. Find the biggest group among these. This group will be the longest subsequence where the difference between adjacent numbers is decreasing.

Code Implementation

def longest_subsequence_decreasing_adjacent_difference_brute_force(sequence):
    longest_subsequence = []

    # Generate all possible subsequences
    for i in range(1 << len(sequence)):
        subsequence = []
        for j in range(len(sequence)):
            if (i >> j) & 1:
                subsequence.append(sequence[j])

        # Check if the subsequence has decreasing adjacent differences
        if len(subsequence) > 1:
            is_decreasing = True
            for k in range(len(subsequence) - 1):
                if subsequence[k] - subsequence[k+1] >= subsequence[k+1] - subsequence[k+2] if k+2 < len(subsequence) else 0:
                    is_decreasing = False
                    break
            if is_decreasing:

                # Update longest_subsequence if needed
                if len(subsequence) > len(longest_subsequence):
                    longest_subsequence = subsequence
        elif len(subsequence) == 1 and len(longest_subsequence) == 0:
            longest_subsequence = subsequence

    return longest_subsequence

Big(O) Analysis

Time Complexity
O(2^n * n)The brute force approach involves generating all possible subsequences of the input sequence of size n. There are 2^n such subsequences. For each subsequence, we need to check if the differences between adjacent elements are strictly decreasing. Checking this condition requires iterating through the subsequence, which, in the worst case, could be of length n. Therefore, the overall time complexity is O(2^n * n).
Space Complexity
O(2^N * N)The brute force approach generates all possible subsequences. In the worst case, there are 2^N possible subsequences, where N is the length of the input sequence. For each subsequence, we need to store the subsequence itself for checking if adjacent differences are decreasing, which takes up to N space in the worst case (if we consider a subsequence with all N elements). Therefore, the space complexity is O(2^N * N).

Optimal Solution

Approach

The goal is to find the longest possible sequence where each number is smaller than the one before it by an increasing amount. We can achieve this efficiently by tracking the best possible sequence ending at each number as we go through the given list.

Here's how the algorithm would work step-by-step:

  1. Think of each number as a potential ending point for our special sequence.
  2. For each number, look back at all the numbers that came before it.
  3. Check if adding the current number to a previous sequence would follow the rule: the difference between the current number and the previous number must be smaller than the previous difference.
  4. If it follows the rule, see if this new sequence is longer than any sequence we've found so far ending at the current number.
  5. If it's longer, update the best sequence ending at the current number.
  6. Keep doing this for every number in the list, always building up the best possible sequences.
  7. At the end, find the longest sequence among all the sequences we've built. That's our answer.

Code Implementation

def longest_subsequence_with_decreasing_adjacent_difference(numbers):
    number_count = len(numbers)
    longest_subsequence_ending_here = [1] * number_count

    # This array stores the length of the longest subsequence ending at each index.
    for current_index in range(1, number_count):
        for previous_index in range(current_index):
            #Check if the current number can extend sequence.
            if numbers[current_index] < numbers[previous_index]:
                if current_index > 1:
                    previous_difference = numbers[previous_index - 1] - numbers[previous_index]
                    current_difference = numbers[previous_index] - numbers[current_index]

                    # Ensure differences are decreasing
                    if current_difference < previous_difference:
                        longest_subsequence_ending_here[current_index] = max(
                            longest_subsequence_ending_here[current_index],
                            longest_subsequence_ending_here[previous_index] + 1
                        )
                else:
                    #If current index is 1, extend seq if current < previous
                    longest_subsequence_ending_here[current_index] = max(
                        longest_subsequence_ending_here[current_index],
                        longest_subsequence_ending_here[previous_index] + 1
                    )
    
    #Find the overall longest subsequence.
    longest_subsequence = 0

    for subsequence_length in longest_subsequence_ending_here:
        longest_subsequence = max(longest_subsequence, subsequence_length)

    return longest_subsequence

Big(O) Analysis

Time Complexity
O(n²)The algorithm iterates through each of the n elements in the input array. For each element, it iterates through all the preceding elements to check if they can extend a valid subsequence. This nested iteration results in approximately n iterations for each of the n elements. Therefore, the time complexity is proportional to n * n, which simplifies to O(n²).
Space Complexity
O(N)The algorithm maintains a data structure to track the best possible sequence ending at each number. Specifically, for each of the N numbers in the input list, we are keeping track of the length of the best subsequence ending at that number. This means we require an auxiliary array or list of size N to store these lengths. Therefore, the auxiliary space complexity is O(N).

Edge Cases

Null input array
How to Handle:
Throw an IllegalArgumentException or return an empty list to avoid NullPointerException
Empty input array
How to Handle:
Return an empty list as no subsequence can be formed.
Array with a single element
How to Handle:
Return a list containing only that single element as it trivially satisfies the condition.
Array with all identical elements
How to Handle:
Return a list containing only the first element as no decreasing difference can be obtained.
Array with monotonically increasing sequence
How to Handle:
Return a list containing only the first element, since no element will have decreasing difference with its successor.
Array with very large numbers leading to potential integer overflow during difference calculation
How to Handle:
Use long data type for difference calculations or employ a check to prevent overflow.
Array with negative numbers
How to Handle:
The difference calculation should handle negative numbers correctly since the decreasing property must hold irrespective of sign.
Maximum sized array (considering memory constraints)
How to Handle:
Ensure that the algorithm's space complexity is optimized to avoid out-of-memory errors for large input sizes and consider using iterative solutions over recursive to avoid stack overflow.