2968. Apply Operations to Maximize Frequency Score


Problem Description

You are given an array of integers nums and an integer k. The array is 0-indexed, meaning that the first element is at position 0. You have the opportunity to perform an operation up to k times on the array. In each operation, you can choose any element in the array and either increase or decrease its value by 1. The goal is to maximize the "score" of the final array, where the score is defined as the frequency of the most frequent element within the array.

To clarify, the frequency of an element is how many times that element appears in the array. You want to make adjustments to the array such that the most frequent element appears as many times as possible, without exceeding the limit of k operations.

The problem asks you to find out the maximum score that could be achieved under these conditions.

Intuition

To solve this problem, one needs to grasp the idea that if we organize the numbers in a particular order, we can efficiently change a segment of numbers to be the same value, which is a strategic way to maximize the frequency of a single number. Sorting the array nums is a good start because it allows us to focus on transforming a contiguous subsequence into a single, repeated number with fewer operations.

Since increasing or decreasing elements by 1 count as a single operation each time, it's preferable to turn a sequence of consecutive numbers into one number—the median of the chosen sequence. This is because the median minimizes the total distance (and thus the total number of operations) needed to make all elements in the range equal to it.

Considering this, if achieving a frequency of x is possible within the k operations, then any frequency less than x should also be possible. This suggests that the relationship between frequency and feasibility is monotonic, which lends itself nicely to a binary search approach to find the maximum feasible frequency.

For the binary search, we define the low boundary l as 0 and the high boundary r as the length of the array n. We keep searching for the middle value mid and check if a subarray of length mid can be transformed such that all of its elements become the median of this subarray with a total operation cost that doesn't exceed k.

To quickly calculate the operation cost for transforming a subarray into a single number, we take advantage of prefix sums—a cumulative sum of the elements in the array that allows us to compute the sum of a subarray in constant time. With two pointers marking the boundaries of the candidate subarray and the prefix sum array, we can efficiently compute the operation cost to transform the subarray elements to the median and check against our operations budget k.

In summary, by sorting the array, leveraging the concept of the median, and applying binary search alongside prefix sums, we can pinpoint the maximum frequency (score) that we can achieve within our operational constraints.

Learn more about Binary Search, Prefix Sum, Sorting and Sliding Window patterns.

Solution Approach

The solution implements a combination of sorting, prefix sum computation, and binary search techniques to achieve the desired outcome. Here's how each component contributes to the final solution:

  1. Sorting: To deal with a range of elements efficiently, the array nums is first sorted. By sorting the array, we ensure that any segment we pick can be changed to the median value of that segment with minimal operations. This is because the median value minimizes the sum of absolute differences to all other elements in a sorted sequence.

  2. Prefix Sum: We employ a prefix sum array s to enable quick calculations of the sum of numbers within any subarray [i, j] of our sorted array. The prefix sum aids in computing the operation cost for two halves of the segment: the left half where we subtract the median, and the right half where we add to the median. It's crucial for evaluating whether we can convert a subarray of a certain size to the same number within k operations.

  3. Binary Search: Knowing that the problem has a monotonic property (if a certain frequency is possible then all lesser frequencies are also possible), we use binary search to find the maximum frequency that is feasible. We define the low boundary l and the high boundary r and search in the middle mid. If a subarray of length mid can be converted to a single number with less than k operations, we can potentially achieve a higher frequency and thus move our lower boundary up; otherwise, we lower our upper boundary.

Here's the practical application of these concepts in the code:

  • We start with a sorted array and initiate a prefix sum s.

  • We then use a binary search, where within each iteration we set a mid value. For each potential mid (which denotes a subarray size), we iterate through the array to check if there's a segment of size mid that can be turned into a single valued sequence, with the median being the target value.

  • For each subarray [i, j] considered, left is calculated as the sum of operations needed to decrease each element to the median, and right is calculated as the sum of operations needed to increase each element to the median.

    • The left sum is calculated using the formula: ((i + j) / 2 - i) * nums[(i + j) / 2] - (s[(i + j) / 2] - s[i]).

    • The right sum is calculated using the formula: (s[j] - s[(i + j) / 2]) - ((j - (i + j) / 2) * nums[(i + j) / 2]).

  • If the sum of left and right is within the k operation limit, it means that a subarray of length mid can indeed be transformed to have the same value; thus, we know we can achieve at least this frequency—and maybe more. So, the binary search will continue to look for a larger subarray that also satisfies the condition, until it finds the largest feasible subarray size before exceeding the k operations limit.

  • The result l, which is maintained by the binary search, will hold the maximum frequency score that could be achieved once the search completes.

By intertwining these algorithms and computational strategies, the solution efficiently navigates through the possibilities of subarray sizes to find the one that yields the maximum frequency within the provided constraints.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's consider a small example to illustrate the solution approach. Suppose we are given the array nums = [1, 3, 4, 4] and k = 3.

Following the solution approach:

  1. Sorting: The array is already sorted, so we do not need to sort it. Our sorted array looks the same: [1, 3, 4, 4].

  2. Prefix Sum: Calculate the prefix sum array s. The s[i] represents the sum of elements from nums[0] to nums[i], inclusively. Thus, for our array, the prefix sum s will look like this: [1, 4, 8, 12].

  3. Binary Search: We use binary search to find the largest subarray, which we can turn into a single number while staying within our operation limit k = 3.

    • Initially, our l (lower boundary for the binary search) is 0, and our r (upper boundary) is the length of the array, which is 4.

    • In the first iteration of binary search, mid would be (l + r) / 2 = (0 + 4) / 2 = 2. We need to check if we can transform a subarray of length 2 into a single number with no more than 3 operations.

    • Let us consider the subarray [3, 4]. The median here is 3.5 (or either 3 or 4, since we're working with integers). We'll choose 4 since it requires fewer operations in this case.

    • To calculate the number of operations to turn this subarray into all 4s, we look at the prefix sums. There's no operation needed for turning 4 into 4, and we need to add 1 to 3 to make it 4. Thus, the number of operations is 0 + 1 = 1, which is less than k.

    • Since we were successful, we can try a larger subarray. Now our new l is mid + 1, which is 3.

    • We repeat the binary search but now with l = 3 and r = 4. Our new mid is (3 + 4) / 2 = 3.5, rounded down to 3.

    • Checking for a subarray of length 3 starting with [1, 3, 4], the median is 3. To make 1 to 3, we need 2 operations, and no operations are needed for turning 3 and 4 into 3. That's 2 operations in total, which is still less than k.

    • At last, we try for a subarray of full length 4. The median of [1, 3, 4, 4] would be 3.5, and let's choose the median to be 3 this time. Now the operations are 2 (for 1 to 3) + 0 (for the first 3) + 1 (for first 4 to 3) + 1 (for second 4 to 3), which adds up to 4 operations. This exceeds k, so we can't transform the entire array into one number within the operation limit.

Through this binary search process, we determine that the largest subarray we can transform into a single number within k operations is of length 3, making the maximum frequency of the most frequent element 3. Therefore, the maximum score we can achieve for this example is 3.

Solution Implementation

1from typing import List
2from itertools import accumulate
3
4class Solution:
5    def max_frequency_score(self, nums: List[int], k: int) -> int:
6        # Sort the array in non-decreasing order.
7        nums.sort()
8        # Create a prefix sum array with an initial value of 0 for easier calculations.
9        prefix_sum = list(accumulate(nums, initial=0))
10        n = len(nums)  # Number of elements in the array.
11        left, right = 0, n  # Binary search boundaries.
12
13        # Use binary search to find the maximum length of the subarray.
14        while left < right:
15            mid = (left + right + 1) // 2  # Choose the middle index.
16            is_valid = False  # Flag to check if a valid subarray is found.
17          
18            # Try every subarray with the length "mid" to check validity.
19            for i in range(n - mid + 1):
20                j = i + mid
21                median = nums[(i + j) // 2]
22              
23                # Calculate the cost to increase all elements before the median.
24                cost_left = (median * ((i + j) // 2 - i)) - (prefix_sum[(i + j) // 2] - prefix_sum[i])
25              
26                # Calculate the cost to decrease all elements after the median to the median.
27                cost_right = (prefix_sum[j] - prefix_sum[(i + j) // 2]) - (median * (j - (i + j) // 2))
28              
29                # Check if the total cost to modify the subarray is within the limit k.
30                if cost_left + cost_right <= k:
31                    is_valid = True
32                    break  # A valid subarray is found, no need to check further.
33          
34            # If a valid subarray is found, we try to find a larger one.
35            if is_valid:
36                left = mid
37            else:
38                # Otherwise, we shrink the search range to look for a smaller subarray.
39                right = mid - 1
40
41        # The left index will contain the maximum subarray length at the end.
42        return left
43
1class Solution {
2    public int maxFrequencyScore(int[] nums, long k) {
3        // Sort the array first.
4        Arrays.sort(nums);
5        int length = nums.length;
6        // Initialize a prefix sum array with an extra space for ease of calculations.
7        long[] prefixSum = new long[length + 1];
8        // Populate the prefix sum array.
9        for (int i = 1; i <= length; i++) {
10            prefixSum[i] = prefixSum[i - 1] + nums[i - 1];
11        }
12        // Initialize the binary search boundaries.
13        int left = 0, right = length;
14        // Perform binary search.
15        while (left < right) {
16            int middle = (left + right + 1) >> 1;
17            boolean isPossible = false;
18          
19            // Try for all subarrays of size 'middle'
20            for (int i = 0; i <= length - middle; i++) {
21                int j = i + middle;
22                // Choose the middle element as the target number to which other elements will be increased.
23                int targetNum = nums[(i + j) / 2];
24                // Calculate how much we need to add to the left half of the subarray to make it all equal to targetNum.
25                long costLeft = ((i + j) / 2 - i) * (long) targetNum - (prefixSum[(i + j) / 2] - prefixSum[i]);
26                // Similarly, calculate the cost for right half of the subarray.
27                long costRight = (prefixSum[j] - prefixSum[(i + j) / 2]) - ((j - (i + j) / 2) * (long) targetNum);
28                // If total operations needed are less or equal to k, it's possible to make all elements of the subarray equal.
29                if (costLeft + costRight <= k) {
30                    isPossible = true;
31                    break;
32                }
33            }
34          
35            // If it's possible to make the elements of a subarray equal with at most k operations, move left boundary up.
36            if (isPossible) {
37                left = middle;
38            } else {
39                // Otherwise, reduce the size of the subarray.
40                right = middle - 1;
41            }
42        }
43
44        // The maximum frequency is the size of the largest subarray for which
45        // we can make all the elements equal with at most k operations.
46        return left;
47    }
48}
49
1class Solution {
2public:
3    // Function to find the maximum frequency score of an element after performing at most k increments.
4    int maxFrequencyScore(vector<int>& nums, long long k) {
5        // Sort the array.
6        sort(nums.begin(), nums.end());
7        int n = nums.size();
8        // Prefix sum array to store the sum of elements up to the i-th index.
9        vector<long long> prefixSum(n + 1, 0);
10        for (int i = 1; i <= n; i++) {
11            prefixSum[i] = prefixSum[i - 1] + nums[i - 1];
12        }
13
14        // Using binary search to find the maximum size of the subsequence with the same value after k increments.
15        int left = 0, right = n;
16        while (left < right) {
17            int mid = (left + right + 1) / 2;
18            bool canForm = false; // Variable to check if a subsequence of size mid can be formed.
19
20            // Check every subsequence of size mid.
21            for (int i = 0; i <= n - mid; i++) {
22                int newRight = i + mid;
23                // Find the median value in the current subsequence.
24                int medianValue = nums[(i + newRight) / 2];
25                long long costLeft = ((i + newRight) / 2 - i) * (long long) medianValue - (prefixSum[(i + newRight) / 2] - prefixSum[i]);
26                long long costRight = (prefixSum[newRight] - prefixSum[(i + newRight) / 2]) - ((newRight - (i + newRight) / 2) * (long long) medianValue);
27
28                // Check if it's possible to make all the elements equal to medianValue with at most k increments.
29                if (costLeft + costRight <= k) {
30                    canForm = true; // It's possible; thus, mid is a feasible solution.
31                    break;
32                }
33            }
34
35            // Update the search range based on whether mid can be achieved.
36            if (canForm) {
37                left = mid; // The capacity to form a larger subsequence may exist.
38            } else {
39                right = mid - 1; // Decrease the size as mid cannot be formed.
40            }
41        }
42
43        // Return the maximum size of the subsequence we found.
44        return left;
45    }
46};
47
1function maxFrequencyScore(nums: number[], k: number): number {
2    // Sort the array in ascending order
3    nums.sort((a, b) => a - b);
4
5    // The length of the nums array
6    const length = nums.length;
7
8    // The prefix sum array with an extra 0 at the start
9    const prefixSums: number[] = Array(length + 1).fill(0);
10
11    // Build the prefix sum array
12    for (let i = 1; i <= length; i++) {
13        prefixSums[i] = prefixSums[i - 1] + nums[i - 1];
14    }
15
16    // Initialize binary search pointers
17    let left: number = 0; 
18    let right: number = length;
19
20    // Perform binary search to find the maximum frequency score
21    while (left < right) {
22        // Midpoint for the current binary search window
23        const midpoint: number = Math.floor((left + right + 1) / 2);
24
25        // Flag to check if a valid subarray exists with the current midpoint
26        let isValid: boolean = false;
27
28        // Test all subarrays of size 'midpoint'
29        for (let i = 0; i <= length - midpoint; i++) {
30            const end = i + midpoint;
31            const medianIndex = Math.floor((i + end) / 2);
32            const medianValue = nums[medianIndex];
33
34            // Calculate the cost to make all elements to the left of medianIndex equal to medianValue
35            const costLeft = medianIndex * medianValue - prefixSums[medianIndex] + prefixSums[i];
36
37            // Calculate the cost to make all elements to the right of medianIndex equal to medianValue
38            const costRight = prefixSums[end] - prefixSums[medianIndex] - (end - medianIndex) * medianValue;
39
40            // Check if total operations needed is less than or equal to k
41            if (costLeft + costRight <= k) {
42                isValid = true;
43                break; // Found valid subarray, break to potentially increase the search space
44            }
45        }
46
47        // Narrow or expand search space based on validity of current midpoint
48        if (isValid) {
49            left = midpoint; // Expand search to larger subarrays
50        } else {
51            right = midpoint - 1; // Narrow search to smaller subarrays
52        }
53    }
54
55    // Return the largest size of the highest frequency element that can be achieved within k operations
56    return left;
57}
58

Time and Space Complexity

The time complexity of the code provided is O(n log n + n log n). This is because there are two main parts to the algorithm contributing to the time complexity:

  1. nums.sort() takes O(n log n) time to sort the array.
  2. The while loop runs for O(log n) iterations (binary search on the size of the frequency), and within each iteration, there is a for loop that could run up to n times dependent on the value of mid. The operations inside the for loop are constant time, therefore the time complexity for this part is O(n log n).

The space complexity of the code is O(n). This stems from the auxiliary space used to store cumulative sums of the array nums in the list s. Since s has the same length as nums, it takes up O(n) space. No other data structures in the code use space that scales with the input size.

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


Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:
Question 1 out of 10

What does the following code do?

1def f(arr1, arr2):
2  i, j = 0, 0
3  new_arr = []
4  while i < len(arr1) and j < len(arr2):
5      if arr1[i] < arr2[j]:
6          new_arr.append(arr1[i])
7          i += 1
8      else:
9          new_arr.append(arr2[j])
10          j += 1
11  new_arr.extend(arr1[i:])
12  new_arr.extend(arr2[j:])
13  return new_arr
14
1public static List<Integer> f(int[] arr1, int[] arr2) {
2  int i = 0, j = 0;
3  List<Integer> newArr = new ArrayList<>();
4
5  while (i < arr1.length && j < arr2.length) {
6      if (arr1[i] < arr2[j]) {
7          newArr.add(arr1[i]);
8          i++;
9      } else {
10          newArr.add(arr2[j]);
11          j++;
12      }
13  }
14
15  while (i < arr1.length) {
16      newArr.add(arr1[i]);
17      i++;
18  }
19
20  while (j < arr2.length) {
21      newArr.add(arr2[j]);
22      j++;
23  }
24
25  return newArr;
26}
27
1function f(arr1, arr2) {
2  let i = 0, j = 0;
3  let newArr = [];
4  
5  while (i < arr1.length && j < arr2.length) {
6      if (arr1[i] < arr2[j]) {
7          newArr.push(arr1[i]);
8          i++;
9      } else {
10          newArr.push(arr2[j]);
11          j++;
12      }
13  }
14  
15  while (i < arr1.length) {
16      newArr.push(arr1[i]);
17      i++;
18  }
19  
20  while (j < arr2.length) {
21      newArr.push(arr2[j]);
22      j++;
23  }
24  
25  return newArr;
26}
27

Recommended Readings

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