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
hastotal_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.
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:
- Calculate the potential gain for each class
- Always pick the class with maximum gain
- Add a student to that class
- Recalculate its new gain (since adding more students to the same class has diminishing returns)
- 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:
- Extract the class with maximum gain (minimum negative gain) from the heap
- Add one brilliant student to this class (increment both
a
andb
) - Recalculate the new gain for this class
- 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 EvaluatorExample 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)
- Each
- 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.
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
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
https assets algo monster cover_photos heap svg Priority Queue and Heap What is the relationship between priority queue and heap Priority Queue is an Abstract Data Type and Heap is the concrete data structure we use to implement a priority queue Priority Queue A priority queue is a data structure
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!