Taro Logo

Recover the Original Array

Hard
Asked by:
Profile picture
14 views
Topics:
ArraysGreedy Algorithms

Alice had a 0-indexed array arr consisting of n positive integers. She chose an arbitrary positive integer k and created two new 0-indexed integer arrays lower and higher in the following manner:

  1. lower[i] = arr[i] - k, for every index i where 0 <= i < n
  2. higher[i] = arr[i] + k, for every index i where 0 <= i < n

Unfortunately, Alice lost all three arrays. However, she remembers the integers that were present in the arrays lower and higher, but not the array each integer belonged to. Help Alice and recover the original array.

Given an array nums consisting of 2n integers, where exactly n of the integers were present in lower and the remaining in higher, return the original array arr. In case the answer is not unique, return any valid array.

Note: The test cases are generated such that there exists at least one valid array arr.

Example 1:

Input: nums = [2,10,6,4,8,12]
Output: [3,7,11]
Explanation:
If arr = [3,7,11] and k = 1, we get lower = [2,6,10] and higher = [4,8,12].
Combining lower and higher gives us [2,6,10,4,8,12], which is a permutation of nums.
Another valid possibility is that arr = [5,7,9] and k = 3. In that case, lower = [2,4,6] and higher = [8,10,12]. 

Example 2:

Input: nums = [1,1,3,3]
Output: [2,2]
Explanation:
If arr = [2,2] and k = 1, we get lower = [1,1] and higher = [3,3].
Combining lower and higher gives us [1,1,3,3], which is equal to nums.
Note that arr cannot be [1,3] because in that case, the only possible way to obtain [1,1,3,3] is with k = 0.
This is invalid since k must be positive.

Example 3:

Input: nums = [5,435]
Output: [220]
Explanation:
The only possible combination is arr = [220] and k = 215. Using them, we get lower = [5] and higher = [435].

Constraints:

  • 2 * n == nums.length
  • 1 <= n <= 1000
  • 1 <= nums[i] <= 109
  • The test cases are generated such that there exists at least one valid array arr.

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 in the input array `nums`? Can I expect negative numbers, zeros, or only positive integers?
  2. Can the input array `nums` be empty or null? If so, what should I return?
  3. Are there guaranteed to be elements that satisfy the problem constraints, allowing recovery of the original array? If there isn't a valid original array, what should be returned?
  4. If multiple original arrays can be recovered, is there a specific criteria for choosing which one to return? For example, should it be the lexicographically smallest?
  5. Are duplicate numbers allowed in the input array `nums`, and if so, how should they be handled when trying to recover the original array?

Brute Force Solution

Approach

The goal is to reconstruct a hidden number sequence from a scrambled list of numbers. The brute force method involves trying every possible secret number that could have been used to create that scrambled list.

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

  1. First, we assume one of the numbers in the scrambled list is the special secret number we need to find.
  2. Next, we try to rebuild the original sequence using that assumed secret number and see if we can create the provided scrambled list.
  3. If we can't reconstruct the original sequence correctly with this secret number, we try the next number in the scrambled list.
  4. We keep doing this, trying each number as the secret number, until we find the correct one that lets us recreate the original sequence and matches the given scrambled list.
  5. If none of them work, then there may not be a valid answer.

Code Implementation

def recover_the_original_array_brute_force(scrambled_list):
    list_length = len(scrambled_list)
    for assumed_secret_number_index in range(list_length):
        assumed_secret_number = scrambled_list[assumed_secret_number_index]

        original_array = []
        temp_scrambled_list = scrambled_list[:]  # Create a copy to modify

        original_array.append(assumed_secret_number)
        temp_scrambled_list.remove(assumed_secret_number)

        # Try to reconstruct the original array
        while temp_scrambled_list:
            found_match = False
            for potential_match in temp_scrambled_list[:]:
                # Check if the difference can be an element.
                difference = abs(original_array[-1] - potential_match)
                if difference in original_array:
                    pass
                else:
                    original_array.append(potential_match)
                    temp_scrambled_list.remove(potential_match)
                    found_match = True
                    break

            if not found_match:
                original_array = []
                break

        # Check if the rebuilt array could generate the given scrambled list
        if len(original_array) > 0:
            reconstructed_scrambled_list = []
            for first_number_index in range(len(original_array)):
                for second_number_index in range(first_number_index, len(original_array)):
                    reconstructed_scrambled_list.append(abs(original_array[first_number_index] - original_array[second_number_index]))

            reconstructed_scrambled_list.sort()
            temp_list = scrambled_list[:]
            temp_list.sort()

            # Account for needing to sort the lists to compare them.
            if reconstructed_scrambled_list == temp_list:
                return original_array

    # Must return an empty list if no match is found
    return []

Big(O) Analysis

Time Complexity
O(n²)The algorithm iterates through each of the n elements in the scrambled array, attempting to use each as the potential 'secret number'. For each chosen 'secret number', the algorithm attempts to reconstruct the original array, which involves iterating through the remaining elements of the scrambled array to check differences and rebuild the array. This reconstruction process effectively involves another iteration of, on average, n/2 elements. Therefore, the total time complexity is approximately n * (n/2), which simplifies to O(n²).
Space Complexity
O(N)The algorithm's space complexity is dominated by the need to reconstruct the original sequence and verify if it matches the given scrambled list. In the process of trying each potential secret number, a temporary array or list of size N is created to hold the reconstructed sequence, where N is the number of elements in the scrambled list. Therefore, the auxiliary space required scales linearly with the input size N.

Optimal Solution

Approach

We're given a mixed-up list of numbers, and our goal is to figure out the original list that was used to create it. The key insight is to focus on the difference between the original numbers and the mixed-up list to deduce a 'k' value that helps us reconstruct the original list by reversing the mixing process.

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

  1. First, organize the mixed-up list from smallest to largest; this makes it easier to spot patterns.
  2. Then, try to figure out the hidden value 'k', which is half the difference between a number in the list and another one. Focus on the smallest number in the mixed-up list, because it must have come directly from the original list.
  3. See if adding 'k' to this smallest number gives you another number that exists in the sorted mixed-up list. If you can't find it, that means the 'k' you guessed is wrong, so try another possibility.
  4. Once you find the correct 'k', start building your original list. Each time you find a number for the original list, remove both that number and the number that's 'k' larger than it from the mixed-up list.
  5. Repeat this process until you've reconstructed the entire original list.

Code Implementation

def recover_the_original_array(mixed_array):
    mixed_array.sort()
    number_of_elements = len(mixed_array)
    original_array = []

    for i in range(1, number_of_elements // 2 + 1):
        potential_k = (mixed_array[i] - mixed_array[0])

        if potential_k == 0 or potential_k % 2 != 0:
            continue

        potential_k //= 2
        temp_mixed_array = mixed_array[:]
        temp_original_array = []
        index = 0

        while index < len(temp_mixed_array):
            first_number = temp_mixed_array[index]
            second_number = first_number + 2 * potential_k

            # Find the second number in the remaining mixed array.
            try:
                second_number_index = temp_mixed_array.index(second_number)

                # Append the first number to the original array.
                temp_original_array.append(first_number)

                # Remove both numbers.
                del temp_mixed_array[second_number_index]
                del temp_mixed_array[index]

                index = 0 # Reset index
            except ValueError:
                index += 1

        # Check if we were able to reconstruct the original array.
        if len(temp_mixed_array) == 0:
            original_array = temp_original_array
            return original_array

    return original_array

Big(O) Analysis

Time Complexity
O(n^2)Sorting the input list of n elements takes O(n log n). Finding the correct 'k' involves iterating through potential differences derived from the smallest element, and in the worst case, this requires examining each element to find its pair, leading to O(n) in the inner loop. Reconstructing the original array involves iterating n/2 times, and within each iteration we potentially need to search the mixed array for matching pairs. The search might take up to O(n) in each reconstructive step. The dominant factor then will be the reconstructive step, with n/2 iterations of potentially O(n) search each. Therefore, the time complexity is approximately n/2 * n which is O(n^2).
Space Complexity
O(N)The algorithm sorts the input list of size N, which may require O(N) auxiliary space depending on the sorting algorithm used (e.g., merge sort). Additionally, the algorithm constructs a new list representing the original array. In the worst-case scenario, this list can also have a size of N/2, which is still linearly proportional to N. Thus, the dominant space usage comes from the sorted list and the original array reconstruction, both contributing O(N) space.

Edge Cases

Null or empty input array
How to Handle:
Return an empty array to indicate no original array can be recovered from an invalid input
Input array with only two elements, no valid solution
How to Handle:
Return an empty array if no valid 'k' can be found that satisfies the problem's condition
Input array with all elements being the same number, but no valid 'k'
How to Handle:
Return an empty array if all differences between pairs of numbers do not yield a single, consistent 'k'.
Integer overflow when calculating potential 'k' values
How to Handle:
Use long data type for intermediate calculations of differences to avoid overflow and then cast back to int if necessary
Input array containing negative numbers
How to Handle:
The solution should handle negative numbers correctly as the difference can also be negative, update counter accordingly
Large input array with potential performance bottleneck
How to Handle:
Utilize optimized data structures like HashMaps for efficient lookups of element counts instead of linear searches
Multiple valid 'k' values exist, resulting in different original arrays
How to Handle:
The solution should choose the smallest possible 'k' to ensure a deterministic output as it does not mention which k to use
Input array with zero values and zero difference
How to Handle:
Handle the case where elements are the same and equal to zero carefully as it involves checking if sufficient number of elements are equal