Facebook Pixel

3634. Minimum Removals to Balance Array

Problem Description

You have an integer array nums and an integer k.

An array is balanced when its maximum element is at most k times its minimum element. In other words, for an array to be balanced: max(array) ≤ k × min(array).

Your task is to remove some elements from nums to make it balanced. You can remove any number of elements, but the array cannot become empty.

Return the minimum number of elements you need to remove to make the remaining array balanced.

Key Points:

  • A single-element array is always balanced (since max = min, the condition holds for any k)
  • You must keep at least one element in the array
  • You want to minimize the number of removals

Example reasoning: If you have nums = [1, 5, 10] and k = 2:

  • The current array is not balanced because max(10) > k × min(1)10 > 2 × 1
  • If you remove 10, the array becomes [1, 5] which is balanced: 5 ≤ 2 × 1 is false
  • If you remove 1, the array becomes [5, 10] which is balanced: 10 ≤ 2 × 5 is true
  • So the minimum removals needed is 1
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that in a balanced array, once we fix the minimum value, the maximum value is constrained by the relationship max ≤ k × min. This means we can determine which elements are valid to keep once we know the minimum.

Let's think about this step by step:

  1. Why sorting helps: If we sort the array, we can easily identify potential minimum values and their corresponding valid maximum values. For any element we choose as the minimum, all valid elements that can coexist with it must be within the range [min, k × min].

  2. The greedy observation: Instead of thinking about which elements to remove, we can flip the problem - what's the maximum number of elements we can keep? The answer is the length of the array minus this maximum.

  3. Trying each element as minimum: For each element nums[i] in the sorted array, if we treat it as the minimum value of our balanced subarray, then:

    • All elements to its left are smaller (can't be included as they would become the new minimum)
    • We can include elements to its right only if they satisfy element ≤ k × nums[i]
  4. Finding the valid range efficiently: Since the array is sorted, for a minimum value nums[i], we can use binary search to find the rightmost position j where nums[j] ≤ k × nums[i]. All elements from index i to j-1 can form a balanced array with nums[i] as the minimum.

  5. Maximizing the kept elements: By trying each element as the potential minimum and finding its corresponding maximum valid range, we can determine the longest possible balanced subarray. The elements in the range [i, j) represent a valid balanced array of length j - i.

The beauty of this approach is that it transforms a removal problem into a selection problem - finding the longest contiguous subarray in the sorted array that satisfies the balance condition.

Learn more about Sorting and Sliding Window patterns.

Solution Approach

The solution implements the intuition using sorting and binary search:

  1. Sort the array: First, we sort nums in ascending order. This allows us to systematically try each element as the minimum value and efficiently find valid maximum values.

  2. Initialize counter: We use cnt to track the maximum number of elements we can keep in a balanced array.

  3. Enumerate each element as minimum: We iterate through each element nums[i] and treat it as the minimum value of a potential balanced array:

    for i, x in enumerate(nums):
  4. Find the valid range using binary search: For each minimum value x, we need to find how many elements can coexist with it. The maximum allowed value is k * x. We use bisect_right to find the index j of the first element that is strictly greater than k * x:

    j = bisect_right(nums, k * x)
    • bisect_right(nums, k * x) returns the insertion point for k * x in the sorted array
    • All elements from index i to j-1 (inclusive) satisfy the condition element ≤ k * x
    • These elements form a valid balanced array with nums[i] as the minimum
  5. Track the maximum keepable elements: The number of elements we can keep when nums[i] is the minimum is j - i. We update our maximum:

    cnt = max(cnt, j - i)
  6. Calculate minimum removals: Since cnt represents the maximum number of elements we can keep, the minimum number of elements to remove is:

    return len(nums) - cnt

Time Complexity: O(n log n) where n is the length of the array

  • Sorting takes O(n log n)
  • For each of the n elements, we perform a binary search which takes O(log n)
  • Total: O(n log n) + O(n * log n) = O(n log n)

Space Complexity: O(1) if we don't count the space used for sorting (typically O(log n) for the sorting algorithm's recursion stack)

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the solution with nums = [3, 1, 7, 4] and k = 3.

Step 1: Sort the array

  • Original: [3, 1, 7, 4]
  • Sorted: [1, 3, 4, 7]

Step 2: Try each element as the minimum

We'll iterate through each element and check how many elements we can keep if it's the minimum.

Iteration 1: i = 0, minimum = 1

  • Maximum allowed value: k × min = 3 × 1 = 3
  • Use binary search to find elements ≤ 3 in [1, 3, 4, 7]
  • bisect_right([1, 3, 4, 7], 3) = 2 (position after the last 3)
  • Valid range: indices [0, 2) → elements [1, 3]
  • Elements we can keep: 2 - 0 = 2

Iteration 2: i = 1, minimum = 3

  • Maximum allowed value: k × min = 3 × 3 = 9
  • Use binary search to find elements ≤ 9 in [1, 3, 4, 7]
  • bisect_right([1, 3, 4, 7], 9) = 4 (all elements are ≤ 9)
  • Valid range: indices [1, 4) → elements [3, 4, 7]
  • Elements we can keep: 4 - 1 = 3

Iteration 3: i = 2, minimum = 4

  • Maximum allowed value: k × min = 3 × 4 = 12
  • Use binary search to find elements ≤ 12 in [1, 3, 4, 7]
  • bisect_right([1, 3, 4, 7], 12) = 4
  • Valid range: indices [2, 4) → elements [4, 7]
  • Elements we can keep: 4 - 2 = 2

Iteration 4: i = 3, minimum = 7

  • Maximum allowed value: k × min = 3 × 7 = 21
  • Use binary search to find elements ≤ 21 in [1, 3, 4, 7]
  • bisect_right([1, 3, 4, 7], 21) = 4
  • Valid range: indices [3, 4) → elements [7]
  • Elements we can keep: 4 - 3 = 1

Step 3: Find the maximum keepable elements

  • Maximum from all iterations: max(2, 3, 2, 1) = 3

Step 4: Calculate minimum removals

  • Total elements: 4
  • Maximum keepable: 3
  • Minimum removals: 4 - 3 = 1

Verification: If we remove element 1, we get [3, 4, 7]. Check if balanced:

  • max = 7, min = 3
  • 7 ≤ 3 × 3 = 9 ✓ (balanced!)

The answer is 1 removal needed.

Solution Implementation

1from typing import List
2from bisect import bisect_right
3
4
5class Solution:
6    def minRemoval(self, nums: List[int], k: int) -> int:
7        # Sort the array to enable binary search
8        nums.sort()
9      
10        # Track the maximum number of elements that can be kept
11        max_elements_to_keep = 0
12      
13        # For each element as the minimum value in a valid subarray
14        for i, min_value in enumerate(nums):
15            # Find the rightmost position where nums[j] <= k * min_value
16            # This gives us the range [i, j) where all elements satisfy
17            # min_value <= nums[x] <= k * min_value
18            right_bound = bisect_right(nums, k * min_value)
19          
20            # Update the maximum number of elements we can keep
21            # right_bound - i gives the count of valid elements starting from index i
22            max_elements_to_keep = max(max_elements_to_keep, right_bound - i)
23      
24        # Return minimum removals = total elements - maximum elements we can keep
25        return len(nums) - max_elements_to_keep
26
1class Solution {
2    public int minRemoval(int[] nums, int k) {
3        // Sort the array to enable binary search and range finding
4        Arrays.sort(nums);
5      
6        // Track the maximum size of valid subarray where max/min <= k
7        int maxValidSubarraySize = 0;
8        int arrayLength = nums.length;
9      
10        // For each element as potential minimum value in the subarray
11        for (int i = 0; i < arrayLength; ++i) {
12            int rightBoundaryIndex = arrayLength;
13          
14            // Check if current element * k can reach the maximum element
15            // This prevents overflow by using long multiplication
16            if (1L * nums[i] * k <= nums[arrayLength - 1]) {
17                // Find the first element greater than nums[i] * k using binary search
18                int targetValue = nums[i] * k + 1;
19                rightBoundaryIndex = Arrays.binarySearch(nums, targetValue);
20              
21                // If target not found, binarySearch returns -(insertion point) - 1
22                // Convert to actual insertion point (first element > nums[i] * k)
23                rightBoundaryIndex = rightBoundaryIndex < 0 ? -rightBoundaryIndex - 1 : rightBoundaryIndex;
24            }
25          
26            // Update maximum valid subarray size
27            // rightBoundaryIndex - i gives the count of elements in range [nums[i], nums[i] * k]
28            maxValidSubarraySize = Math.max(maxValidSubarraySize, rightBoundaryIndex - i);
29        }
30      
31        // Return minimum removals needed (total elements - maximum valid subarray)
32        return arrayLength - maxValidSubarraySize;
33    }
34}
35
1class Solution {
2public:
3    int minRemoval(vector<int>& nums, int k) {
4        // Sort the array to enable binary search and range calculations
5        ranges::sort(nums);
6      
7        // Track the maximum number of elements that can form a valid subarray
8        int maxValidElements = 0;
9        int n = nums.size();
10      
11        // For each element as the minimum value of a potential subarray
12        for (int i = 0; i < n; ++i) {
13            // Find the rightmost position where elements are still within k times the minimum
14            int rightBoundary = n;
15          
16            // Check if the maximum possible value (nums[i] * k) is within array bounds
17            // Use 1LL to prevent integer overflow during multiplication
18            if (1LL * nums[i] * k <= nums[n - 1]) {
19                // Binary search for the first element greater than nums[i] * k
20                rightBoundary = upper_bound(nums.begin(), nums.end(), 1LL * nums[i] * k) - nums.begin();
21            }
22          
23            // Update the maximum count of valid elements in a subarray
24            // The valid range is from index i to rightBoundary (exclusive)
25            maxValidElements = max(maxValidElements, rightBoundary - i);
26        }
27      
28        // Return minimum removals needed (total elements minus maximum valid subarray)
29        return n - maxValidElements;
30    }
31};
32
1/**
2 * Finds the minimum number of elements to remove from an array
3 * such that the maximum element is at most k times the minimum element
4 * @param nums - The input array of numbers
5 * @param k - The multiplication factor constraint
6 * @returns The minimum number of elements to remove
7 */
8function minRemoval(nums: number[], k: number): number {
9    // Sort the array in ascending order
10    nums.sort((a: number, b: number) => a - b);
11  
12    const arrayLength: number = nums.length;
13    let maxValidSubarraySize: number = 0;
14  
15    // Try each element as the potential minimum of the valid subarray
16    for (let leftIndex: number = 0; leftIndex < arrayLength; ++leftIndex) {
17        let rightBoundIndex: number = arrayLength;
18      
19        // Check if current minimum times k is less than the maximum element
20        if (nums[leftIndex] * k <= nums[arrayLength - 1]) {
21            // Find the upper bound index where elements exceed nums[leftIndex] * k
22            const threshold: number = nums[leftIndex] * k + 1;
23            rightBoundIndex = binarySearchUpperBound(nums, threshold);
24        }
25      
26        // Update the maximum size of valid subarray found
27        maxValidSubarraySize = Math.max(maxValidSubarraySize, rightBoundIndex - leftIndex);
28    }
29  
30    // Return the minimum number of elements to remove
31    return arrayLength - maxValidSubarraySize;
32}
33
34/**
35 * Binary search to find the index of the first element greater than or equal to target
36 * @param sortedArray - The sorted array to search in
37 * @param target - The target value to search for
38 * @returns The index of the first element >= target
39 */
40function binarySearchUpperBound(sortedArray: number[], target: number): number {
41    let left: number = 0;
42    let right: number = sortedArray.length;
43  
44    while (left < right) {
45        const mid: number = Math.floor((left + right) / 2);
46        if (sortedArray[mid] < target) {
47            left = mid + 1;
48        } else {
49            right = mid;
50        }
51    }
52  
53    return left;
54}
55

Time and Space Complexity

The time complexity is O(n × log n), where n is the length of the array nums. This complexity arises from two main operations:

  • Sorting the array nums.sort() takes O(n × log n) time
  • The for loop runs n times, and within each iteration, bisect_right() performs a binary search which takes O(log n) time, resulting in O(n × log n) for the loop
  • The overall complexity is O(n × log n) + O(n × log n) = O(n × log n)

The space complexity is O(log n), where n is the length of the array nums. This space is primarily used by:

  • The sorting algorithm (typically Timsort in Python) which uses O(log n) space for its recursive call stack
  • The bisect_right() function uses O(1) space as it performs iterative binary search
  • Variables cnt, i, j, and x use O(1) space
  • The overall space complexity is O(log n)

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Integer Overflow When Computing k × min_value

The Problem: When calculating k * min_value, the multiplication can cause integer overflow if both k and min_value are large integers. In Python, this isn't typically an issue due to arbitrary precision integers, but in languages like Java or C++, this can lead to incorrect results.

Example Scenario:

  • min_value = 10^9
  • k = 10^9
  • k * min_value = 10^18 (exceeds 32-bit integer range and approaches 64-bit limits)

Solution:

# Instead of direct multiplication, use comparison to avoid overflow
# Rather than: bisect_right(nums, k * min_value)
# Use a custom binary search with division:

def find_right_bound(nums, start_idx, k, min_value):
    left, right = start_idx, len(nums)
    while left < right:
        mid = (left + right) // 2
        # Avoid multiplication by using division
        if nums[mid] <= k * min_value:  # Could overflow
        # Better: if nums[mid] / min_value <= k (but watch for division by zero)
        # Or: if min_value == 0 or nums[mid] <= k * min_value
            left = mid + 1
        else:
            right = mid
    return left

2. Not Handling Edge Cases with k = 0

The Problem: When k = 0, the only valid balanced array would be one where all elements are equal (since max ≤ 0 × min only holds when max = 0 or when max = min = 0).

Solution:

def minRemoval(self, nums: List[int], k: int) -> int:
    if k == 0:
        # Find the most frequent element and keep only those
        from collections import Counter
        counter = Counter(nums)
        max_frequency = max(counter.values())
        return len(nums) - max_frequency
  
    # Continue with the original solution...
    nums.sort()
    max_elements_to_keep = 0
    # ...

3. Misunderstanding the Binary Search Boundary

The Problem: Using bisect_left instead of bisect_right or misinterpreting the returned index. bisect_right returns the index where we should insert to keep the array sorted, which is the first index where nums[index] > k * min_value.

Incorrect Usage:

# Wrong: This finds elements < k * min_value, missing equal elements
j = bisect_left(nums, k * min_value)

Correct Usage:

# Correct: This includes all elements <= k * min_value
j = bisect_right(nums, k * min_value)

4. Not Considering Negative Numbers

The Problem: When the array contains negative numbers, the min-max relationship becomes tricky. If min_value is negative and max_value is positive, the condition max ≤ k × min might behave unexpectedly.

Example:

  • nums = [-10, -5, 5], k = 2
  • Using -10 as min: 5 ≤ 2 × (-10)5 ≤ -20 (false)
  • Using -5 as min: 5 ≤ 2 × (-5)5 ≤ -10 (false)

Solution: The original algorithm handles this correctly by trying each element as the minimum, but be aware that with negative numbers, you might need to keep only negative or only positive elements to achieve balance.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which data structure is used in a depth first search?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More