Facebook Pixel

1792. Maximum Average Pass Ratio

Problem Description

You have a school with multiple classes, and each class has students taking a final exam. You're given a 2D array classes where classes[i] = [pass_i, total_i] means:

  • Class i has total_i students in total
  • Currently, pass_i students will pass the exam

You also have extraStudents brilliant students who are guaranteed to pass any exam. You need to assign these extra students to classes to maximize the overall average pass ratio.

Key definitions:

  • Pass ratio of a class = (number of students who pass) / (total number of students)
  • Average pass ratio = (sum of all class pass ratios) / (number of classes)

Your goal is to distribute the extraStudents among the classes to achieve the highest possible average pass ratio. Each extra student you add to a class increases both the number of passing students and the total number of students by 1.

For example, if a class has 3 students passing out of 5 total (ratio = 3/5 = 0.6), and you add one brilliant student, it becomes 4 students passing out of 6 total (ratio = 4/6 ≈ 0.667).

The solution uses a greedy approach with a max-heap. It calculates the "gain" or improvement in pass ratio for adding one student to each class. The gain from changing a class from a/b to (a+1)/(b+1) is (a+1)/(b+1) - a/b. The algorithm repeatedly assigns students to the class that would benefit most (highest gain) until all extra students are assigned.

Return the maximum average pass ratio achievable. Answers within 10^-5 of the actual answer are accepted.

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

Intuition

The key insight is that adding a student to different classes will have different impacts on the overall average. We want to maximize our return on each student placement.

Consider two classes:

  • Class A: 1 student passes out of 2 (ratio = 0.5)
  • Class B: 90 students pass out of 100 (ratio = 0.9)

If we add one brilliant student:

  • Class A becomes 2/3 ≈ 0.667 (gain of ~0.167)
  • Class B becomes 91/101 ≈ 0.901 (gain of ~0.001)

Even though Class B has a higher pass ratio, adding a student to Class A provides a much larger improvement. This reveals that we should focus on which class gets the biggest "boost" from each additional student.

The gain formula (a+1)/(b+1) - a/b measures exactly this boost. Classes with lower initial ratios and smaller total sizes tend to benefit more from additional students.

Since we want to maximize the total average, we should use a greedy strategy: always give the next student to whichever class would improve the most. This naturally leads to using a max-heap data structure, where we:

  1. Calculate the potential gain for each class
  2. Always pick the class with maximum gain
  3. Add a student to that class
  4. Recalculate its new gain (since adding more students to the same class has diminishing returns)
  5. Repeat until all extra students are assigned

The greedy approach works here because each decision is independent - adding a student to one class doesn't affect the ratios of other classes, only the potential gain of the same class for future additions.

Learn more about Greedy and Heap (Priority Queue) patterns.

Solution Approach

The solution implements a greedy algorithm using a max-heap (priority queue) to efficiently track and update the gain from adding students to each class.

Step 1: Initialize the Max-Heap

We create a heap where each element is a tuple containing:

  • The negative gain value (since Python's heapq is a min-heap by default, we negate to simulate max-heap)
  • Current number of passing students a
  • Current total number of students b

The initial gain for each class is calculated as: a/b - (a+1)/(b+1)

h = [(a / b - (a + 1) / (b + 1), a, b) for a, b in classes]
heapify(h)

Step 2: Distribute Extra Students

For each of the extraStudents, we:

  1. Extract the class with maximum gain (minimum negative gain) from the heap
  2. Add one brilliant student to this class (increment both a and b)
  3. Recalculate the new gain for this class
  4. Push the updated class back into the heap
for _ in range(extraStudents):
    _, a, b = heappop(h)
    a, b = a + 1, b + 1
    heappush(h, (a / b - (a + 1) / (b + 1), a, b))

The gain decreases with each additional student to the same class. For example, if a class goes from 3/5 to 4/6, the next gain would be 4/6 - 5/7, which is smaller than the previous gain of 3/5 - 4/6.

Step 3: Calculate Final Average

After all extra students are assigned, we calculate the average pass ratio:

  • Sum all individual class ratios: sum(v[1] / v[2] for v in h)
  • Divide by the number of classes: / len(classes)
return sum(v[1] / v[2] for v in h) / len(classes)

Time Complexity: O(n log n + m log n) where n is the number of classes and m is the number of extra students. The initial heapification takes O(n), and we perform m operations of heap pop and push, each taking O(log n).

Space Complexity: O(n) for storing the heap with information about all classes.

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 to illustrate the solution approach.

Input:

  • classes = [[1, 2], [3, 5], [2, 2]]
  • extraStudents = 2

We have 3 classes:

  • Class 0: 1/2 = 0.500 pass ratio
  • Class 1: 3/5 = 0.600 pass ratio
  • Class 2: 2/2 = 1.000 pass ratio

Step 1: Calculate Initial Gains and Build Max-Heap

For each class, calculate the gain from adding one student:

  • Class 0: (2/3) - (1/2) = 0.667 - 0.500 = 0.167
  • Class 1: (4/6) - (3/5) = 0.667 - 0.600 = 0.067
  • Class 2: (3/3) - (2/2) = 1.000 - 1.000 = 0.000

Max-heap (stored as negative values): [(-0.167, 1, 2), (-0.067, 3, 5), (0.000, 2, 2)]

Step 2: Assign First Extra Student

Pop class with maximum gain (Class 0 with gain 0.167):

  • Add student: Class 0 becomes 2/3
  • Calculate new gain: (3/4) - (2/3) = 0.750 - 0.667 = 0.083
  • Push back: (-0.083, 2, 3)

Heap state: [(-0.083, 2, 3), (-0.067, 3, 5), (0.000, 2, 2)]

Step 3: Assign Second Extra Student

Pop class with maximum gain (Class 0 again with gain 0.083):

  • Add student: Class 0 becomes 3/4
  • Calculate new gain: (4/5) - (3/4) = 0.800 - 0.750 = 0.050
  • Push back: (-0.050, 3, 4)

Final heap: [(-0.067, 3, 5), (-0.050, 3, 4), (0.000, 2, 2)]

Step 4: Calculate Final Average

Final class configurations:

  • Class 0: 3/4 = 0.750
  • Class 1: 3/5 = 0.600
  • Class 2: 2/2 = 1.000

Average pass ratio = (0.750 + 0.600 + 1.000) / 3 = 2.350 / 3 ≈ 0.783

Notice how both extra students went to Class 0, even though it initially had the lowest pass ratio. This is because it provided the greatest improvement to the overall average each time.

Solution Implementation

1class Solution:
2    def maxAverageRatio(self, classes: List[List[int]], extraStudents: int) -> float:
3        # Initialize max heap (using negative values since Python has min heap)
4        # Store tuples of (negative gain, pass_count, total_count)
5        # Gain = current_ratio - potential_ratio_after_adding_one_student
6        heap = []
7        for pass_count, total_count in classes:
8            # Calculate the gain (improvement) if we add one student to this class
9            current_ratio = pass_count / total_count
10            new_ratio = (pass_count + 1) / (total_count + 1)
11            gain = current_ratio - new_ratio  # Negative gain for max heap behavior
12            heap.append((gain, pass_count, total_count))
13      
14        # Convert list to heap structure
15        heapify(heap)
16      
17        # Distribute extra students to maximize average pass ratio
18        for _ in range(extraStudents):
19            # Pop the class with maximum potential improvement
20            _, pass_count, total_count = heappop(heap)
21          
22            # Add one passing student to this class
23            pass_count += 1
24            total_count += 1
25          
26            # Recalculate gain and push back to heap
27            current_ratio = pass_count / total_count
28            new_ratio = (pass_count + 1) / (total_count + 1)
29            gain = current_ratio - new_ratio
30            heappush(heap, (gain, pass_count, total_count))
31      
32        # Calculate the final average pass ratio across all classes
33        total_ratio = sum(item[1] / item[2] for item in heap)
34        average_ratio = total_ratio / len(classes)
35      
36        return average_ratio
37
1class Solution {
2    public double maxAverageRatio(int[][] classes, int extraStudents) {
3        // Create a max heap based on the improvement gain of adding one student
4        // The improvement gain is calculated as: (pass+1)/(total+1) - pass/total
5        PriorityQueue<double[]> maxHeap = new PriorityQueue<>((classA, classB) -> {
6            // Calculate improvement gain for classA
7            double gainA = (classA[0] + 1) / (classA[1] + 1) - classA[0] / classA[1];
8            // Calculate improvement gain for classB
9            double gainB = (classB[0] + 1) / (classB[1] + 1) - classB[0] / classB[1];
10            // Sort in descending order (max heap)
11            return Double.compare(gainB, gainA);
12        });
13      
14        // Add all classes to the priority queue
15        // Convert int arrays to double arrays for precision
16        for (int[] classInfo : classes) {
17            maxHeap.offer(new double[] {classInfo[0], classInfo[1]});
18        }
19      
20        // Distribute extra students to classes that benefit most
21        while (extraStudents-- > 0) {
22            // Get the class with maximum improvement potential
23            double[] topClass = maxHeap.poll();
24            // Add one student who will pass to this class
25            double newPassCount = topClass[0] + 1;
26            double newTotalCount = topClass[1] + 1;
27            // Put the updated class back into the heap
28            maxHeap.offer(new double[] {newPassCount, newTotalCount});
29        }
30      
31        // Calculate the sum of all pass ratios
32        double totalRatio = 0;
33        while (!maxHeap.isEmpty()) {
34            double[] classInfo = maxHeap.poll();
35            totalRatio += classInfo[0] / classInfo[1];
36        }
37      
38        // Return the average pass ratio
39        return totalRatio / classes.length;
40    }
41}
42
1class Solution {
2public:
3    double maxAverageRatio(vector<vector<int>>& classes, int extraStudents) {
4        // Max heap to store classes by their potential improvement
5        // Each tuple contains: (improvement gain, passed students, total students)
6        priority_queue<tuple<double, int, int>> maxHeap;
7      
8        // Initialize heap with all classes and their potential improvements
9        for (auto& classInfo : classes) {
10            int passedStudents = classInfo[0];
11            int totalStudents = classInfo[1];
12          
13            // Calculate improvement if we add one extra student to this class
14            // Improvement = new_ratio - current_ratio
15            double improvement = (double)(passedStudents + 1) / (totalStudents + 1) 
16                               - (double)passedStudents / totalStudents;
17          
18            maxHeap.push({improvement, passedStudents, totalStudents});
19        }
20      
21        // Distribute extra students to maximize average pass ratio
22        while (extraStudents--) {
23            // Get the class with maximum potential improvement
24            auto [currentImprovement, passedStudents, totalStudents] = maxHeap.top();
25            maxHeap.pop();
26          
27            // Add one extra passing student to this class
28            passedStudents++;
29            totalStudents++;
30          
31            // Recalculate the new improvement for adding another student
32            double newImprovement = (double)(passedStudents + 1) / (totalStudents + 1) 
33                                  - (double)passedStudents / totalStudents;
34          
35            // Push updated class back to heap
36            maxHeap.push({newImprovement, passedStudents, totalStudents});
37        }
38      
39        // Calculate the final average pass ratio
40        double totalRatio = 0.0;
41        while (!maxHeap.empty()) {
42            auto [improvement, passedStudents, totalStudents] = maxHeap.top();
43            maxHeap.pop();
44          
45            // Add this class's pass ratio to total
46            totalRatio += (double)passedStudents / totalStudents;
47        }
48      
49        // Return the average pass ratio across all classes
50        return totalRatio / classes.size();
51    }
52};
53
1function maxAverageRatio(classes: number[][], extraStudents: number): number {
2    // Custom max heap implementation since TypeScript doesn't have built-in priority queue
3    class MaxHeap {
4        private heap: [number, number, number][] = [];
5      
6        // Get parent index
7        private getParentIndex(index: number): number {
8            return Math.floor((index - 1) / 2);
9        }
10      
11        // Get left child index
12        private getLeftChildIndex(index: number): number {
13            return 2 * index + 1;
14        }
15      
16        // Get right child index
17        private getRightChildIndex(index: number): number {
18            return 2 * index + 2;
19        }
20      
21        // Swap two elements in heap
22        private swap(index1: number, index2: number): void {
23            [this.heap[index1], this.heap[index2]] = [this.heap[index2], this.heap[index1]];
24        }
25      
26        // Bubble up element to maintain heap property
27        private bubbleUp(index: number): void {
28            while (index > 0) {
29                const parentIndex = this.getParentIndex(index);
30                // Compare by improvement value (first element of tuple)
31                if (this.heap[parentIndex][0] >= this.heap[index][0]) break;
32                this.swap(parentIndex, index);
33                index = parentIndex;
34            }
35        }
36      
37        // Bubble down element to maintain heap property
38        private bubbleDown(index: number): void {
39            while (true) {
40                let largestIndex = index;
41                const leftChildIndex = this.getLeftChildIndex(index);
42                const rightChildIndex = this.getRightChildIndex(index);
43              
44                // Check if left child is larger
45                if (leftChildIndex < this.heap.length && 
46                    this.heap[leftChildIndex][0] > this.heap[largestIndex][0]) {
47                    largestIndex = leftChildIndex;
48                }
49              
50                // Check if right child is larger
51                if (rightChildIndex < this.heap.length && 
52                    this.heap[rightChildIndex][0] > this.heap[largestIndex][0]) {
53                    largestIndex = rightChildIndex;
54                }
55              
56                // If current element is largest, stop
57                if (largestIndex === index) break;
58              
59                this.swap(index, largestIndex);
60                index = largestIndex;
61            }
62        }
63      
64        // Add element to heap
65        push(element: [number, number, number]): void {
66            this.heap.push(element);
67            this.bubbleUp(this.heap.length - 1);
68        }
69      
70        // Remove and return top element
71        pop(): [number, number, number] | undefined {
72            if (this.heap.length === 0) return undefined;
73            if (this.heap.length === 1) return this.heap.pop();
74          
75            const top = this.heap[0];
76            this.heap[0] = this.heap.pop()!;
77            this.bubbleDown(0);
78            return top;
79        }
80      
81        // Check if heap is empty
82        isEmpty(): boolean {
83            return this.heap.length === 0;
84        }
85    }
86  
87    // Create max heap to store classes by their potential improvement
88    // Each tuple contains: [improvement gain, passed students, total students]
89    const maxHeap = new MaxHeap();
90  
91    // Initialize heap with all classes and their potential improvements
92    for (const classInfo of classes) {
93        const passedStudents = classInfo[0];
94        const totalStudents = classInfo[1];
95      
96        // Calculate improvement if we add one extra student to this class
97        // Improvement = new_ratio - current_ratio
98        const improvement = (passedStudents + 1) / (totalStudents + 1) 
99                          - passedStudents / totalStudents;
100      
101        maxHeap.push([improvement, passedStudents, totalStudents]);
102    }
103  
104    // Distribute extra students to maximize average pass ratio
105    while (extraStudents > 0) {
106        // Get the class with maximum potential improvement
107        const topElement = maxHeap.pop();
108        if (!topElement) break;
109      
110        let [currentImprovement, passedStudents, totalStudents] = topElement;
111      
112        // Add one extra passing student to this class
113        passedStudents++;
114        totalStudents++;
115      
116        // Recalculate the new improvement for adding another student
117        const newImprovement = (passedStudents + 1) / (totalStudents + 1) 
118                             - passedStudents / totalStudents;
119      
120        // Push updated class back to heap
121        maxHeap.push([newImprovement, passedStudents, totalStudents]);
122      
123        extraStudents--;
124    }
125  
126    // Calculate the final average pass ratio
127    let totalRatio = 0.0;
128    while (!maxHeap.isEmpty()) {
129        const topElement = maxHeap.pop();
130        if (!topElement) break;
131      
132        const [improvement, passedStudents, totalStudents] = topElement;
133      
134        // Add this class's pass ratio to total
135        totalRatio += passedStudents / totalStudents;
136    }
137  
138    // Return the average pass ratio across all classes
139    return totalRatio / classes.length;
140}
141

Time and Space Complexity

The time complexity is O((n + k) × log n), where n is the number of classes and k is the number of extra students.

  • Initial heap creation from the list comprehension: O(n) for creating the list
  • heapify(h): O(n) for building the heap
  • The for loop runs k times (extraStudents iterations):
    • Each heappop() operation: O(log n)
    • Each heappush() operation: O(log n)
    • Total for the loop: O(k × log n)
  • Final sum calculation: O(n)

Overall time complexity: O(n + n + k × log n + n) = O(n + k × log n)

When k ≥ n, this simplifies to O(k × log n). However, the reference answer states O(n × log n), which suggests either k = O(n) (extra students is proportional to number of classes) or there's an assumption that k ≤ n in the problem context.

The space complexity is O(n).

  • The heap h stores one tuple for each class: O(n)
  • No additional significant space is used beyond the heap

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

Common Pitfalls

1. Incorrect Gain Calculation Direction

One of the most common mistakes is calculating the gain in the wrong direction. Since we want to maximize the improvement, we need to calculate how much the ratio increases when adding a student.

Wrong approach:

gain = current_ratio - new_ratio  # This gives negative gain for improvement

Correct approach:

gain = new_ratio - current_ratio  # Positive gain for improvement
# Then negate for max-heap: -gain

Or directly calculate for min-heap usage:

gain = current_ratio - new_ratio  # Negative value, smaller means better

2. Heap Initialization Error

Another pitfall is incorrectly structuring the heap tuple, which can lead to wrong comparisons or index errors when extracting values.

Wrong approach:

# Missing gain calculation or wrong tuple order
heap = [(pass_count, total_count) for pass_count, total_count in classes]

Correct approach:

heap = []
for pass_count, total_count in classes:
    gain = pass_count / total_count - (pass_count + 1) / (total_count + 1)
    heap.append((gain, pass_count, total_count))
heapify(heap)

3. Integer Division Instead of Float Division

In Python 2 or when not careful with division, integer division can cause precision loss.

Wrong approach (Python 2 style):

gain = pass_count / total_count  # May perform integer division

Correct approach:

gain = pass_count / total_count  # Python 3 automatically uses float division
# Or explicitly: float(pass_count) / total_count

4. Not Recalculating Gain After Each Addition

A critical mistake is reusing the same gain value or incorrectly calculating the new gain after adding a student.

Wrong approach:

for _ in range(extraStudents):
    _, pass_count, total_count = heappop(heap)
    pass_count += 1
    total_count += 1
    # Reusing old gain formula or not updating it
    heappush(heap, (gain, pass_count, total_count))  # 'gain' is stale

Correct approach:

for _ in range(extraStudents):
    _, pass_count, total_count = heappop(heap)
    pass_count += 1
    total_count += 1
    # Recalculate gain with updated values
    new_gain = pass_count / total_count - (pass_count + 1) / (total_count + 1)
    heappush(heap, (new_gain, pass_count, total_count))

5. Floating Point Precision Issues

When dealing with ratios and divisions, floating-point arithmetic can introduce small errors that might affect the final result.

Potential issue:

# Multiple divisions can accumulate rounding errors
ratio = pass_count / total_count
new_ratio = (pass_count + 1) / (total_count + 1)
gain = ratio - new_ratio

Better approach (though the original is usually sufficient):

# Calculate gain in one expression to minimize intermediate rounding
gain = (pass_count * (total_count + 1) - total_count * (pass_count + 1)) / (total_count * (total_count + 1))
# Simplifies to: (pass_count - total_count) / (total_count * (total_count + 1))

However, for this problem, the standard floating-point calculation is acceptable since the answer tolerance is 10^-5.

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

Given a sorted array of integers and an integer called target, find the element that equals to the target and return its index. Select the correct code that fills the ___ in the given code snippet.

1def binary_search(arr, target):
2    left, right = 0, len(arr) - 1
3    while left ___ right:
4        mid = (left + right) // 2
5        if arr[mid] == target:
6            return mid
7        if arr[mid] < target:
8            ___ = mid + 1
9        else:
10            ___ = mid - 1
11    return -1
12
1public static int binarySearch(int[] arr, int target) {
2    int left = 0;
3    int right = arr.length - 1;
4
5    while (left ___ right) {
6        int mid = left + (right - left) / 2;
7        if (arr[mid] == target) return mid;
8        if (arr[mid] < target) {
9            ___ = mid + 1;
10        } else {
11            ___ = mid - 1;
12        }
13    }
14    return -1;
15}
16
1function binarySearch(arr, target) {
2    let left = 0;
3    let right = arr.length - 1;
4
5    while (left ___ right) {
6        let mid = left + Math.trunc((right - left) / 2);
7        if (arr[mid] == target) return mid;
8        if (arr[mid] < target) {
9            ___ = mid + 1;
10        } else {
11            ___ = mid - 1;
12        }
13    }
14    return -1;
15}
16

Recommended Readings

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

Load More