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:
-
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. -
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 withink
operations. -
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 boundaryr
and search in the middlemid
. If a subarray of lengthmid
can be converted to a single number with less thank
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 potentialmid
(which denotes a subarray size), we iterate through the array to check if there's a segment of sizemid
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, andright
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
andright
is within thek
operation limit, it means that a subarray of lengthmid
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 thek
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 EvaluatorExample 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:
-
Sorting: The array is already sorted, so we do not need to sort it. Our sorted array looks the same:
[1, 3, 4, 4]
. -
Prefix Sum: Calculate the prefix sum array
s
. Thes[i]
represents the sum of elements fromnums[0]
tonums[i]
, inclusively. Thus, for our array, the prefix sums
will look like this:[1, 4, 8, 12]
. -
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 ourr
(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 length2
into a single number with no more than 3 operations. -
Let us consider the subarray
[3, 4]
. The median here is3.5
(or either3
or4
, since we're working with integers). We'll choose4
since it requires fewer operations in this case. -
To calculate the number of operations to turn this subarray into all
4
s, we look at the prefix sums. There's no operation needed for turning4
into4
, and we need to add1
to3
to make it4
. Thus, the number of operations is0 + 1 = 1
, which is less thank
. -
Since we were successful, we can try a larger subarray. Now our new
l
ismid + 1
, which is3
. -
We repeat the binary search but now with
l = 3
andr = 4
. Our newmid
is(3 + 4) / 2 = 3.5
, rounded down to3
. -
Checking for a subarray of length
3
starting with[1, 3, 4]
, the median is3
. To make1
to3
, we need2
operations, and no operations are needed for turning3
and4
into3
. That's2
operations in total, which is still less thank
. -
At last, we try for a subarray of full length
4
. The median of[1, 3, 4, 4]
would be3.5
, and let's choose the median to be3
this time. Now the operations are2
(for1
to3
) +0
(for the first3
) +1
(for first4
to3
) +1
(for second4
to3
), which adds up to4
operations. This exceedsk
, 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:
nums.sort()
takesO(n log n)
time to sort the array.- 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 ton
times dependent on the value ofmid
. The operations inside the for loop are constant time, therefore the time complexity for this part isO(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.
Which algorithm is best for finding the shortest distance between two points in an unweighted graph?
Recommended Readings
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
Prefix Sum The prefix sum is an incredibly powerful and straightforward technique Its primary goal is to allow for constant time range sum queries on an array What is Prefix Sum The prefix sum of an array at index i is the sum of all numbers from index 0 to i By
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Want a Structured Path to Master System Design Too? Don’t Miss This!