Facebook Pixel

2968. Apply Operations to Maximize Frequency Score

Problem Description

You have an array of integers nums (0-indexed) and an integer k representing the maximum number of operations you can perform.

In each operation, you can:

  • Select any index i in the array
  • Either increase or decrease nums[i] by 1

Your goal is to maximize the frequency of the most frequent element in the final array. The frequency of an element is simply how many times it appears in the array.

For example, if you have nums = [1, 2, 4] and k = 5:

  • You could change the array to [2, 2, 2] by decreasing 4 twice (2 operations) and increasing 1 once (1 operation), using 3 operations total
  • This gives you a maximum frequency of 3 (the element 2 appears 3 times)

The key insight is that you want to make as many elements equal as possible within your operation budget k. Each operation moves an element one step closer to your target value, so elements that are already close to each other will require fewer operations to become equal.

The solution uses binary search to find the maximum possible frequency. For each candidate frequency, it checks if it's possible to create a subarray of that length where all elements can be made equal using at most k operations. The optimal strategy is to change all elements to the median value of the subarray, as this minimizes the total number of operations needed.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The first key observation is that if we want to make multiple elements equal, it's most efficient to work with elements that are already close to each other in value. This suggests we should sort the array first, as consecutive elements in a sorted array have the smallest differences.

Once sorted, we realize that to maximize frequency, we should focus on making a contiguous segment of the sorted array equal. Why? Because if we try to make non-contiguous elements equal, we'd be wasting operations on elements in between that we're not including.

Now, what value should we change all elements in a segment to? The median is optimal. Consider a segment [1, 2, 5]:

  • If we change all to 1: operations needed = 0 + 1 + 4 = 5
  • If we change all to 2: operations needed = 1 + 0 + 3 = 4
  • If we change all to 5: operations needed = 4 + 3 + 0 = 7

The median minimizes the total distance (and thus operations) needed.

The next insight is about the search strategy. If we can achieve a frequency of x (meaning we can make x elements equal), then we can definitely achieve any frequency less than x - we'd just use fewer elements. This monotonic property screams binary search!

So our approach becomes:

  1. Binary search on the answer (the maximum frequency)
  2. For each candidate frequency mid, check if it's achievable
  3. To check if frequency mid is achievable, try all possible windows of size mid in the sorted array
  4. For each window, calculate the cost to make all elements equal to the median
  5. If any window has cost ≤ k, then frequency mid is achievable

The cost calculation uses prefix sums for efficiency. For a window from index i to j, we need:

  • Elements below the median to increase to reach the median
  • Elements above the median to decrease to reach the median

This gives us the formula: left + right where:

  • left = sum of differences between median and elements below it
  • right = sum of differences between elements above median and the median itself

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

Solution Approach

Let's walk through the implementation step by step:

1. Sorting and Prefix Sum Setup

First, we sort the array to group similar values together:

nums.sort()

Then we build a prefix sum array for efficient range sum queries:

s = list(accumulate(nums, initial=0))

This allows us to calculate the sum of any subarray nums[i:j] as s[j] - s[i] in O(1) time.

2. Binary Search Framework

We binary search on the possible frequency values:

l, r = 0, n
while l < r:
    mid = (l + r + 1) >> 1
  • l = 0: minimum possible frequency
  • r = n: maximum possible frequency (all elements become equal)
  • mid = (l + r + 1) >> 1: middle value with upward rounding

3. Checking Feasibility of a Frequency

For each candidate frequency mid, we check all possible windows of size mid:

for i in range(n - mid + 1):
    j = i + mid
    x = nums[(i + j) // 2]  # median element

4. Calculating Operations Needed

For each window [i, j), we calculate the cost to make all elements equal to the median x:

left = ((i + j) // 2 - i) * x - (s[(i + j) // 2] - s[i])
right = (s[j] - s[(i + j) // 2]) - ((j - (i + j) // 2) * x)

Breaking down the formulas:

  • Left cost: Elements from index i to (i+j)//2 - 1 need to increase to x

    • Number of elements: (i + j) // 2 - i
    • Target sum if all were x: ((i + j) // 2 - i) * x
    • Current sum: s[(i + j) // 2] - s[i]
    • Operations needed: target sum - current sum
  • Right cost: Elements from index (i+j)//2 + 1 to j-1 need to decrease to x

    • Current sum: s[j] - s[(i + j) // 2]
    • Target sum if all were x: (j - (i + j) // 2) * x
    • Operations needed: current sum - target sum

5. Binary Search Decision

If any window can be transformed with at most k operations:

if left + right <= k:
    ok = True
    break

Based on feasibility:

  • If feasible (ok = True): This frequency works, try larger (l = mid)
  • If not feasible: This frequency is too large, try smaller (r = mid - 1)

Time Complexity: O(n² log n)

  • Binary search: O(log n) iterations
  • Each iteration checks O(n) windows
  • Each window check: O(1) using prefix sums

Space Complexity: O(n) for the prefix sum array

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 a concrete example with nums = [1, 4, 8, 13] and k = 5.

Step 1: Sort and Build Prefix Sum

  • Array is already sorted: [1, 4, 8, 13]
  • Prefix sum array: s = [0, 1, 5, 13, 26]
    • This allows us to quickly calculate range sums

Step 2: Binary Search on Frequency

We'll binary search between frequency 1 and 4 (array length).

First iteration: mid = 2

  • Can we make 2 elements equal with ≤ 5 operations?

  • Check all windows of size 2:

    Window [1, 4] (indices 0-1):

    • Median element: nums[0] = 1
    • Cost to make both equal to 1: We need to decrease 4 to 1
    • Operations = 3 ✓ (≤ 5, so frequency 2 is achievable)

Second iteration: mid = 3

  • Can we make 3 elements equal with ≤ 5 operations?

  • Check all windows of size 3:

    Window [1, 4, 8] (indices 0-2):

    • Median element: nums[1] = 4
    • Cost calculation:
      • Left side: Change 1 to 4 = 3 operations
      • Right side: Change 8 to 4 = 4 operations
      • Total = 7 operations ✗ (> 5)

    Window [4, 8, 13] (indices 1-3):

    • Median element: nums[2] = 8
    • Cost calculation:
      • Left side: Change 4 to 8 = 4 operations
      • Right side: Change 13 to 8 = 5 operations
      • Total = 9 operations ✗ (> 5)

    Frequency 3 is not achievable with k=5.

Binary Search Continues:

  • Since frequency 3 failed, we search lower
  • We already know frequency 2 works
  • Binary search converges to answer: 2

Verification of the Answer: With k=5 operations, we can transform [1, 4, 8, 13] to have at most frequency 2:

  • One possibility: Change 4 to 1 (3 operations) → [1, 1, 8, 13]
  • Another possibility: Change 8 to 4 (4 operations) → [1, 4, 4, 13]

The maximum frequency achievable is 2.

Template Solution

def maxFrequency(self, nums: List[int], k: int) -> int:
    nums.sort()
    n = len(nums)
    s = list(accumulate(nums, initial=0))
  
    l, r = 0, n
    while l < r:
        mid = (l + r + 1) >> 1
        ok = False
      
        # Check all windows of size mid
        for i in range(n - mid + 1):
            j = i + mid
            x = nums[(i + j) // 2]  # median element
          
            # Calculate operations needed for left side
            left = ((i + j) // 2 - i) * x - (s[(i + j) // 2] - s[i])
          
            # Calculate operations needed for right side  
            right = (s[j] - s[(i + j) // 2]) - ((j - (i + j) // 2) * x)
          
            if left + right <= k:
                ok = True
                break
      
        if ok:
            l = mid
        else:
            r = mid - 1
  
    return l

Solution Implementation

1class Solution:
2    def maxFrequencyScore(self, nums: List[int], k: int) -> int:
3        # Sort the array for efficient median-based calculations
4        nums.sort()
5      
6        # Create prefix sum array for range sum queries
7        # s[i] represents sum of elements from nums[0] to nums[i-1]
8        prefix_sum = list(accumulate(nums, initial=0))
9        n = len(nums)
10      
11        # Binary search on the maximum possible frequency (window size)
12        left, right = 0, n
13      
14        while left < right:
15            # Calculate mid point (potential window size)
16            # Using (left + right + 1) // 2 to bias towards upper half
17            mid = (left + right + 1) >> 1
18          
19            # Check if we can achieve frequency of 'mid' with at most k operations
20            is_valid = False
21          
22            # Try all possible windows of size 'mid'
23            for window_start in range(n - mid + 1):
24                window_end = window_start + mid
25              
26                # Find the median element's index in current window
27                median_index = (window_start + window_end) // 2
28                median_value = nums[median_index]
29              
30                # Calculate cost to change all elements left of median to median value
31                # Cost = (count of left elements * median) - (sum of left elements)
32                left_count = median_index - window_start
33                left_sum = prefix_sum[median_index] - prefix_sum[window_start]
34                left_cost = left_count * median_value - left_sum
35              
36                # Calculate cost to change all elements right of median to median value
37                # Cost = (sum of right elements) - (count of right elements * median)
38                right_count = window_end - median_index
39                right_sum = prefix_sum[window_end] - prefix_sum[median_index]
40                right_cost = right_sum - right_count * median_value
41              
42                # Check if total cost is within budget k
43                if left_cost + right_cost <= k:
44                    is_valid = True
45                    break
46          
47            # Update binary search bounds based on validity
48            if is_valid:
49                left = mid  # Can achieve this frequency, try larger
50            else:
51                right = mid - 1  # Cannot achieve, try smaller
52      
53        return left
54
1class Solution {
2    public int maxFrequencyScore(int[] nums, long k) {
3        // Sort the array to enable efficient calculation of operations needed
4        Arrays.sort(nums);
5        int n = nums.length;
6      
7        // Build prefix sum array for quick range sum queries
8        // prefixSum[i] stores sum of elements from index 0 to i-1
9        long[] prefixSum = new long[n + 1];
10        for (int i = 1; i <= n; i++) {
11            prefixSum[i] = prefixSum[i - 1] + nums[i - 1];
12        }
13      
14        // Binary search on the answer (maximum possible frequency)
15        int left = 0;
16        int right = n;
17      
18        while (left < right) {
19            // Calculate mid point, rounding up to avoid infinite loop
20            int mid = (left + right + 1) >> 1;
21            boolean canAchieveFrequency = false;
22          
23            // Try all possible subarrays of length mid
24            for (int startIdx = 0; startIdx <= n - mid; startIdx++) {
25                int endIdx = startIdx + mid;
26              
27                // Choose median as target value (optimal for minimizing total operations)
28                int medianIdx = (startIdx + endIdx) / 2;
29                int targetValue = nums[medianIdx];
30              
31                // Calculate operations needed for left half (elements smaller than median)
32                // Operations = (count of elements) * targetValue - (sum of elements)
33                long leftOperations = (long)(medianIdx - startIdx) * targetValue - 
34                                      (prefixSum[medianIdx] - prefixSum[startIdx]);
35              
36                // Calculate operations needed for right half (elements larger than median)
37                // Operations = (sum of elements) - (count of elements) * targetValue
38                long rightOperations = (prefixSum[endIdx] - prefixSum[medianIdx]) - 
39                                      (long)(endIdx - medianIdx) * targetValue;
40              
41                // Check if total operations needed is within budget k
42                if (leftOperations + rightOperations <= k) {
43                    canAchieveFrequency = true;
44                    break;
45                }
46            }
47          
48            // Update binary search bounds based on whether mid frequency is achievable
49            if (canAchieveFrequency) {
50                left = mid;  // Can achieve this frequency, try larger
51            } else {
52                right = mid - 1;  // Cannot achieve this frequency, try smaller
53            }
54        }
55      
56        return left;
57    }
58}
59
1class Solution {
2public:
3    int maxFrequencyScore(vector<int>& nums, long long k) {
4        // Sort the array to group similar values together
5        sort(nums.begin(), nums.end());
6      
7        int n = nums.size();
8      
9        // Build prefix sum array for efficient range sum queries
10        // prefixSum[i] represents the sum of first i elements
11        vector<long long> prefixSum(n + 1, 0);
12        for (int i = 1; i <= n; i++) {
13            prefixSum[i] = prefixSum[i - 1] + nums[i - 1];
14        }
15
16        // Binary search on the answer (maximum possible subsequence length)
17        int left = 0, right = n;
18        while (left < right) {
19            // Calculate mid with ceiling division to avoid infinite loop
20            int mid = (left + right + 1) >> 1;
21            bool isPossible = false;
22
23            // Try all possible windows of size 'mid'
24            for (int i = 0; i <= n - mid; i++) {
25                int j = i + mid;  // End of current window (exclusive)
26              
27                // Choose median as the target value (optimal for minimizing total cost)
28                int medianIndex = (i + j) / 2;
29                int targetValue = nums[medianIndex];
30              
31                // Calculate cost to change all elements in left half to targetValue
32                // Elements from index i to medianIndex-1 need to be increased
33                long long leftCost = (long long)(medianIndex - i) * targetValue - 
34                                    (prefixSum[medianIndex] - prefixSum[i]);
35              
36                // Calculate cost to change all elements in right half to targetValue
37                // Elements from index medianIndex to j-1 need to be decreased
38                long long rightCost = (prefixSum[j] - prefixSum[medianIndex]) - 
39                                     (long long)(j - medianIndex) * targetValue;
40
41                // Check if total cost is within budget
42                if (leftCost + rightCost <= k) {
43                    isPossible = true;
44                    break;
45                }
46            }
47
48            // Update binary search bounds based on feasibility
49            if (isPossible) {
50                left = mid;  // Can potentially achieve larger subsequence
51            } else {
52                right = mid - 1;  // Need to try smaller subsequence
53            }
54        }
55
56        return left;
57    }
58};
59
1/**
2 * Finds the maximum frequency score achievable by performing at most k operations
3 * @param nums - Array of numbers to process
4 * @param k - Maximum number of operations allowed
5 * @returns Maximum frequency score (length of subarray that can be made equal)
6 */
7function maxFrequencyScore(nums: number[], k: number): number {
8    // Sort the array in ascending order
9    nums.sort((a, b) => a - b);
10  
11    const arrayLength: number = nums.length;
12  
13    // Build prefix sum array for quick range sum calculations
14    const prefixSum: number[] = Array(arrayLength + 1).fill(0);
15    for (let i = 1; i <= arrayLength; i++) {
16        prefixSum[i] = prefixSum[i - 1] + nums[i - 1];
17    }
18
19    // Binary search for the maximum achievable frequency
20    let left: number = 0;
21    let right: number = arrayLength;
22  
23    while (left < right) {
24        // Calculate middle value (ceiling division)
25        const mid: number = (left + right + 1) >> 1;
26        let isPossible: boolean = false;
27      
28        // Check if we can make a subarray of length 'mid' equal with at most k operations
29        for (let startIdx = 0; startIdx <= arrayLength - mid; startIdx++) {
30            const endIdx: number = startIdx + mid;
31          
32            // Choose the median element as the target value (minimizes total operations)
33            const medianIdx: number = Math.floor((startIdx + endIdx) / 2);
34            const targetValue: number = nums[medianIdx];
35          
36            // Calculate operations needed for elements left of median
37            const leftOperations: number = (medianIdx - startIdx) * targetValue - 
38                                          (prefixSum[medianIdx] - prefixSum[startIdx]);
39          
40            // Calculate operations needed for elements right of median
41            const rightOperations: number = (prefixSum[endIdx] - prefixSum[medianIdx]) - 
42                                           (endIdx - medianIdx) * targetValue;
43          
44            // Check if total operations needed is within budget
45            if (leftOperations + rightOperations <= k) {
46                isPossible = true;
47                break;
48            }
49        }
50      
51        // Update binary search bounds based on feasibility
52        if (isPossible) {
53            left = mid;
54        } else {
55            right = mid - 1;
56        }
57    }
58
59    return left;
60}
61

Time and Space Complexity

Time Complexity: O(n × log n)

The time complexity consists of two main parts:

  • Sorting the array takes O(n log n) time
  • Binary search on the answer space: The binary search runs for O(log n) iterations (searching from 0 to n for the maximum frequency). In each iteration, we check all possible subarrays of length mid, which takes O(n) time. Therefore, this part contributes O(n × log n) time.

Since both operations are O(n × log n), the overall time complexity is O(n × log n).

Space Complexity: O(n)

The space complexity comes from:

  • The prefix sum array s which stores n+1 elements, requiring O(n) space
  • The sorting operation may require O(log n) space for the recursion stack in the worst case (depending on the sorting algorithm implementation)
  • Other variables like l, r, mid, i, j, x, left, right, and ok use O(1) space

The dominant term is O(n) from the prefix sum array, so the overall space complexity is O(n).

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

Common Pitfalls

1. Incorrect Median Index Calculation for Even-Length Windows

One of the most common mistakes is mishandling the median index calculation when the window has an even number of elements. The code uses (window_start + window_end) // 2, but developers often confuse whether this gives the lower or upper median.

The Issue: For even-length windows, there are two middle elements. The formula (i + j) // 2 gives us the upper median index. This inconsistency can lead to:

  • Incorrect cost calculations
  • Off-by-one errors in counting elements on each side

Solution: Be explicit about which median you're using and ensure consistency:

# For a window [window_start, window_end)
median_index = (window_start + window_end) // 2
# This gives the upper median for even-length windows

# Alternative: use lower median consistently
# median_index = (window_start + window_end - 1) // 2

2. Confusion with Prefix Sum Boundaries

The prefix sum array uses initial=0, making prefix_sum[i] represent the sum of elements from index 0 to i-1 (exclusive of i). This off-by-one relationship often causes errors.

The Issue: Developers might incorrectly calculate range sums:

# Wrong: sum from i to j inclusive
wrong_sum = prefix_sum[j] - prefix_sum[i]  # This actually gives sum from i to j-1

# Correct: sum from i to j inclusive
correct_sum = prefix_sum[j + 1] - prefix_sum[i]

Solution: Always remember that with initial=0, prefix_sum[i] contains the sum of the first i elements (indices 0 to i-1).

3. Binary Search Boundary Update Logic

The code uses mid = (left + right + 1) >> 1 with specific update rules. Using the wrong combination can cause infinite loops.

The Issue: If you use mid = (left + right) // 2 but keep the update logic as left = mid, you'll get an infinite loop when left = right - 1.

Solution: Match your mid calculation with your update logic:

# Option 1: Upper mid with left = mid
mid = (left + right + 1) // 2
if valid:
    left = mid
else:
    right = mid - 1

# Option 2: Lower mid with right = mid
mid = (left + right) // 2
if valid:
    left = mid + 1
else:
    right = mid

4. Not Considering Single-Element Windows

Some implementations forget that a frequency of 1 is always achievable with 0 operations, potentially missing edge cases.

The Issue: Starting binary search with left = 1 instead of left = 0 might seem logical but can cause issues with edge cases or when the result should be 1.

Solution: Always start with left = 0 and let the binary search naturally find the answer, even if it means checking trivial cases.

5. Integer Overflow in Cost Calculations

For large arrays with large values, the cost calculations might overflow in languages with fixed integer sizes.

The Issue: Expressions like left_count * median_value can overflow when both values are large.

Solution (for languages with overflow concerns): Use appropriate data types or check for potential overflow:

# Python handles big integers automatically, but in other languages:
# Use long long in C++ or check for overflow before multiplication
if left_count > 0 and median_value > MAX_VALUE // left_count:
    # Handle overflow case
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What are the most two important steps in writing a depth first search function? (Select 2)


Recommended Readings

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

Load More