Taro Logo

Count Beautiful Splits in an Array

Medium
Amazon logo
Amazon
1 view
Topics:
Arrays

You are given an array nums.

A split of an array nums is beautiful if:

  1. The array nums is split into three subarrays: nums1, nums2, and nums3, such that nums can be formed by concatenating nums1, nums2, and nums3 in that order.
  2. The subarray nums1 is a prefix of nums2 OR nums2 is a prefix of nums3.

Return the number of ways you can make this split.

For example:

nums = [1, 1, 2, 1]

Here, the beautiful splits are:

  • nums1 = [1], nums2 = [1, 2], nums3 = [1]
  • nums1 = [1], nums2 = [1], nums3 = [2, 1]

Therefore the answer is 2.

Another example:

nums = [1, 2, 3, 4]

Here, there are no beautiful splits, so the answer is 0.

How would you implement a function to solve this problem, and what is the time and space complexity of your solution?

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 are the possible ranges of values within the input array, and what data type are they (e.g., integers, floats)?
  2. What defines a 'beautiful split' in more detail? Specifically, can you provide examples of arrays that would result in valid or invalid splits, illustrating the criteria for a 'beautiful' split?
  3. What should be returned if no 'beautiful split' exists in the given array?
  4. Can the input array be empty or null, and if so, what should the expected output be in these cases?
  5. Does the order of elements matter when determining a 'beautiful split'? For instance, are two splits considered the same if they contain the same numbers but in a different order?

Brute Force Solution

Approach

The goal is to find how many ways we can cut a list into two pieces where each piece has the same number of unique things. The brute force approach is like trying every possible cut and then checking if that cut is a good one.

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

  1. Imagine marking a place to split the list: first after the first item, then after the second, then after the third, and so on, all the way to the end.
  2. For each of these split locations, look at everything to the left of the split and count how many different kinds of things are there.
  3. Then look at everything to the right of the split and do the same thing – count how many different kinds of things are there.
  4. If the number of different things on the left is the same as the number of different things on the right, then this split is a good split.
  5. Keep track of how many of these good splits you find as you try all the possible splitting locations.
  6. Once you've gone through all the possible split locations, the number of good splits you've found is your answer.

Code Implementation

def count_beautiful_splits_brute_force(numbers):
    number_of_beautiful_splits = 0
    # Iterate through all possible split positions
    for split_index in range(1, len(numbers)):
        left_subarray = numbers[:split_index]
        right_subarray = numbers[split_index:]

        # Find number of unique elements in left
        unique_elements_left = set(left_subarray)
        count_unique_left = len(unique_elements_left)

        # Find number of unique elements in right
        unique_elements_right = set(right_subarray)
        count_unique_right = len(unique_elements_right)

        # Increment the counter if unique element counts are equal
        if count_unique_left == count_unique_right:
            number_of_beautiful_splits += 1

    return number_of_beautiful_splits

Big(O) Analysis

Time Complexity
O(n²)The algorithm iterates through all possible split points in the array, which takes O(n) time. For each split point, the algorithm calculates the number of distinct elements in both the left and right subarrays. Calculating the number of distinct elements in a subarray requires iterating through the subarray, which takes O(n) time in the worst case. Therefore, the overall time complexity is O(n * n), which simplifies to O(n²).
Space Complexity
O(N)The algorithm iterates through all possible split locations, and for each split, it counts the unique elements on the left and right sides. To count unique elements, it implicitly uses hash sets or similar data structures. In the worst case, each element in the input array of size N could be unique. Therefore, for each split, the algorithm might store up to N unique elements in these hash sets to perform the counting. The space used by the hash sets scales linearly with the input size N, resulting in O(N) auxiliary space.

Optimal Solution

Approach

To efficiently count beautiful splits, the key is to precompute information that helps us avoid redundant calculations. We will count unique characters for each possible split, then use these counts to determine if the split is beautiful.

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

  1. First, we need to know the total unique characters in the entire input string.
  2. Then, as we go through each possible splitting point, we determine the unique characters in the left side of the split.
  3. We also need to check if the number of unique characters on the left is the same as the total unique characters of the original string minus the unique characters on the right. Since determining the right-side unique characters would be costly, we do it by subtraction.
  4. If the number of unique characters on the left side of the split matches the number of unique characters of the remaining unsplit part of the string, it means the split is beautiful.
  5. We simply increment our final count when a split is deemed to be beautiful.
  6. Finally we return the total count of beautiful splits.

Code Implementation

def count_beautiful_splits(word):
    total_length = len(word)
    beautiful_split_count = 0
    
    # Calculate total unique chars
    total_unique_chars = len(set(word))

    for split_index in range(1, total_length):
        left_substring = word[:split_index]
        right_substring = word[split_index:]
        
        # Count unique chars on the left
        left_unique_chars = len(set(left_substring))

        # Check if split is beautiful
        if left_unique_chars == total_unique_chars:
            beautiful_split_count += 1

    return beautiful_split_count

Big(O) Analysis

Time Complexity
O(n)The dominant operation is iterating through the string (or array, equivalently) once to find each possible split. Inside this loop, we determine the unique characters on the left side of the split using a set which takes O(1) time per character and O(n) total time. The remaining operations (calculating the total unique characters in the entire string initially and determining unique characters on the right side by subtraction) each take O(n) time at most. Since the code contains only one loop which iterates n times, the Big O time complexity is O(n).
Space Complexity
O(1)The algorithm uses a fixed number of variables to store the total unique character count of the string, the unique character count on the left side of the split, and potentially a counter for beautiful splits. No auxiliary data structures like arrays or hash maps are created that scale with the input string's length (N). Therefore, the space complexity remains constant, independent of the input size.

Edge Cases

CaseHow to Handle
Null or empty input arrayReturn 0 immediately, as no split is possible.
Array with only one elementReturn 0 immediately, as a split requires at least two parts.
Array with all identical elementsThe solution should iterate through all possible splits and count only valid ones.
Array containing only negative numbersThe algorithm should correctly handle negative numbers when counting distinct prime factors.
Large input array (performance considerations)Optimize prime factorization to avoid timeouts by precomputing primes or using efficient algorithms.
Integer overflow during prime factor calculationUse a larger integer type (e.g., long) or handle overflow cases explicitly to prevent incorrect results.
No valid split exists in the arrayThe solution should return 0 if no split meets the 'beautiful' criteria after checking all possibilities.
Extreme values in the array (very large or very small)Ensure prime factorization handles large numbers efficiently without exceeding integer limits or causing timeouts.