Taro Logo

Number of Valid Subarrays

Hard
Asked by:
Profile picture
16 views
Topics:
ArraysStacks

Given an array of integers nums, return the number of non-empty subarrays that have elements sorted in non-decreasing order.

A subarray is a contiguous part of an array.

Example 1:

Input: nums = [1,4,2,5,3]
Output: 7
Explanation: The 7 valid subarrays are [1], [4], [2], [5], [3], [1,4], and [2,5].

Example 2:

Input: nums = [2,2,2]
Output: 6
Explanation: There are 6 valid subarrays: [2], [2], [2], [2,2], [2,2], and [2,2,2].

Constraints:

  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 106

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 value ranges for the integers in the input array? Can they be negative, zero, or only positive?
  2. What is the maximum size of the input array? Should I be concerned about integer overflow when calculating the number of valid subarrays?
  3. Could you define more precisely what constitutes a 'valid' subarray? Does it strictly mean that each element must be less than or equal to the first element?
  4. If the input array is empty or null, what should the function return?
  5. Are there any specific performance expectations for this problem beyond aiming for optimal time complexity? For example, should I prioritize minimizing space complexity as well?

Brute Force Solution

Approach

The brute force approach involves examining every possible group of consecutive numbers within the larger set. For each group, we check if it meets our criteria to be 'valid'. Finally, we count up all the groups that passed the validity test.

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

  1. Start by selecting the first number in the list and consider it as a group by itself.
  2. Check if this single-number group is valid based on the given rule (e.g., is it the smallest number in the group?).
  3. Next, expand the group by including the number that follows the first number.
  4. Again, check if this two-number group is valid.
  5. Keep adding one more number to the group at a time, each time checking for validity, until you have a group that includes all numbers from the beginning to the end of the list.
  6. Now, repeat the whole process, but this time, start the initial single-number group with the second number in the original list.
  7. Continue this, starting the single-number group with each subsequent number in the original list, until you have tried every possible starting point.
  8. As you check each group, keep a count of how many groups are valid.
  9. The final count represents the total number of valid groups.

Code Implementation

def number_of_valid_subarrays_brute_force(numbers):
    number_of_valid_subarrays = 0

    # Iterate through all possible start indices.
    for start_index in range(len(numbers)):
        for end_index in range(start_index, len(numbers)):

            # Create the subarray from start to end index
            subarray = numbers[start_index : end_index + 1]

            # Assume it is valid until proven otherwise.
            is_valid = True

            # Check for validity: each element must be >= first element
            for number in subarray:
                if number < subarray[0]:
                    is_valid = False
                    break

            #Increment total if current subarray is valid
            if is_valid:
                number_of_valid_subarrays += 1

    return number_of_valid_subarrays

Big(O) Analysis

Time Complexity
O(n²) – The algorithm iterates through all possible subarrays of the input array of size n. For each starting index i, the algorithm iterates from i to n, forming subarrays of increasing length. This creates a nested loop structure. The outer loop runs n times, and the inner loop, on average, runs n/2 times. Thus, the total number of operations is proportional to n * (n/2), which simplifies to O(n²).
Space Complexity
O(1) – The brute force approach, as described, does not use any auxiliary data structures like lists, hash maps, or trees to store intermediate results. The algorithm primarily iterates through subarrays using indices, potentially storing a few integer variables for loop counters or validity checks. The number of such variables remains constant irrespective of the input array's size N. Therefore, the auxiliary space complexity is constant, or O(1).

Optimal Solution

Approach

The goal is to efficiently count the number of valid groups within a sequence. Instead of checking every possible group, we use a clever method to track how many groups starting at each position are valid, avoiding redundant checks.

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

  1. Imagine going through the sequence one item at a time from left to right.
  2. For each item, think about how many consecutive items to its right form a valid group with it.
  3. The key is to notice that if a later item is smaller than an earlier item in the sequence, all the items in between the earlier and later item would make the sequence invalid.
  4. So, for each item, find the first item to its right that's smaller than it. Everything in between it and that smaller item will form a valid group.
  5. Count how many items are in the group for each starting point.
  6. Finally, add up the sizes of all the groups to get the total number of valid groups.

Code Implementation

def number_of_valid_subarrays(numbers):
    number_of_elements = len(numbers)
    valid_subarrays_count = 0

    for start_index in range(number_of_elements):
        # Find valid subarrays starting from each index
        for end_index in range(start_index, number_of_elements):
            is_valid = True
            # Check if the current subarray is valid
            for check_index in range(start_index, end_index + 1):
                if numbers[start_index] > numbers[check_index]:
                    is_valid = False
                    break

            if is_valid:
                valid_subarrays_count += 1

    return valid_subarrays_count

def number_of_valid_subarrays_optimal(numbers):
    number_of_elements = len(numbers)
    valid_subarrays_count = 0
    stack = []

    for index in range(number_of_elements):
        # Monotonically increasing stack to find the next smaller element
        while stack and numbers[stack[-1]] > numbers[index]:
            stack.pop()

        # If stack is empty, it means the current element is the smallest until index.
        if not stack:
            valid_subarrays_count += index + 1

        # Else, the difference gives us the number of valid subarrays.
        else:
            valid_subarrays_count += index - stack[-1]

        stack.append(index)

    return valid_subarrays_count

def number_of_valid_subarrays_stack(numbers):
    number_of_elements = len(numbers)
    valid_subarrays_count = 0
    stack = []

    for index in range(number_of_elements):
        # Remove elements from stack which are greater than current element
        while stack and numbers[stack[-1]] > numbers[index]:
            stack.pop()

        # Stack empty means all elements to the left are greater or equal
        if not stack:
            valid_subarrays_count += index + 1

        # Calculate valid subarrays by subtracting the previous smaller element
        else:
            valid_subarrays_count += index - stack[-1]
        # Storing indices instead of values
        stack.append(index)

    return valid_subarrays_count

def number_of_valid_subarrays_optimized(numbers):
    number_of_elements = len(numbers)
    valid_subarrays_count = 0
    stack = []

    for current_index in range(number_of_elements):
        # Maintain monotonically increasing stack
        while stack and numbers[stack[-1]] > numbers[current_index]:
            stack.pop()

        # If stack is empty, all previous elements are >=, so current_index+1 subarrays are valid
        if not stack:
            valid_subarrays_count += current_index + 1

        # Otherwise, the number of valid subarrays is the difference between current and the previous smaller
        else:
            valid_subarrays_count += current_index - stack[-1]

        # Store the index, not the value.
        stack.append(current_index)

    return valid_subarrays_count

Big(O) Analysis

Time Complexity
O(n²) – The algorithm iterates through each of the n elements in the input sequence. For each element, it potentially examines all subsequent elements to determine the extent of the valid subarray starting at that position. In the worst-case scenario, for each of the n starting positions, it might need to compare with up to n-1 other elements. This results in a nested loop-like behavior with approximately n * (n-1)/2 comparisons. Therefore, the time complexity simplifies to O(n²).
Space Complexity
O(1) – The algorithm iterates through the sequence and for each element, determines the length of the valid subarray starting from that element. The plain English explanation doesn't mention any auxiliary data structures, like temporary lists, stacks or hashmaps, that grow with the input size N. It primarily involves comparing elements and counting valid subarrays which can be done using constant extra space by using a few variables. Thus, the space complexity is O(1).

Edge Cases

Null or empty input array
How to Handle:
Return 0 (or an appropriate error code/exception depending on requirements) as no subarrays can be formed.
Array with only one element
How to Handle:
Return 0 since a valid subarray of size greater than 1 cannot exist.
Array with all elements being the same value
How to Handle:
The solution must correctly count the number of valid subarrays based on that single value.
Array with elements in strictly increasing order
How to Handle:
The first element would form a valid subarray with every other element that follows.
Array with elements in strictly decreasing order
How to Handle:
No element will be greater or equal to elements that follow, so the result should be 0.
Array with negative numbers
How to Handle:
The comparison `>=` in the provided code works correctly with negative numbers.
Large array to consider time complexity - e.g., array of size 10^5
How to Handle:
A naive O(n^2) solution may result in a timeout; an optimized O(n) stack-based solution is required.
Integer overflow if intermediate calculations become too large
How to Handle:
Use appropriate data types (e.g., `long` in Java or `long long` in C++) to prevent integer overflow during the count.