LeetCode - 1792
2024-12-15
Solutions
The core reason why both solutions have the same conceptual approach yet exhibit drastically different performance lies in the subtle but impactful differences in their implementations. Although both use a priority queue and follow a similar logic of repeatedly picking the class with the highest incremental gain for adding an extra student.
Solution 1:
class Solution {
public:
double maxAverageRatio(vector<vector<int>>& classes, int extraStudents) {
auto profit = [&](const double& pass, const double& total) {
return (pass + 1)/(total + 1) - pass/total;
};
double total = 0;
priority_queue<pair<double, array<int, 2>>> pq;
for(int i = 0; i < classes.size(); i++) {
total += (double)classes[i][0]/classes[i][1];
pq.push({profit(classes[i][0], classes[i][1]), {classes[i][0], classes[i][1]}});
}
while(extraStudents--) {
auto [pfit, arr] = pq.top(); pq.pop();
total += pfit;
pq.push({profit(arr[0]+1, arr[1]+1), {arr[0]+1, arr[1]+1}});
}
return total / classes.size();
}
};
Solution 2:
class Solution {
public:
double maxAverageRatio(vector<vector<int>>& classes, int es) {
auto comp = [](vector<double>& a, vector<double>& b){
return (a[0] + 1.0) / (a[1] + 1.0) - (a[0]) / a[1] <
(b[0] + 1.0) / (b[1] + 1.0) - (b[0]) / b[1];
};
priority_queue<vector<double>, vector<vector<double>>, decltype(comp)> pq(comp);
for(auto c : classes) pq.push({double(c[0]), double(c[1])});
while(es--){
auto c = pq.top();
pq.pop();
pq.push({c[0]+1.0, c[1]+1.0});
}
double res = 0;
while(!pq.empty()){
auto c = pq.top();
pq.pop();
res += c[0] /c[1];
}
return res/classes.size();
}
};
1. Comparator Complexity and Repeated Computations
In the second solution, the priority queue’s comparator calculates the incremental improvement ( (pass + 1)/(total + 1) - pass/total )
each time it needs to compare two elements. This means that every push, pop, and internal restructuring of the heap triggers fresh recalculations of the same formula.
- Frequent recalculations: Every comparison in the heap involves multiple floating-point operations, repeated many times.
- Extra function calls: Unlike the first solution, where the gain is precomputed and stored directly, the second solution continuously re-derives these values.
2. Data Structure Choice and Memory Handling
- First solution: Uses
pair<double, array<int,2>>
, a compact, fixed-size structure with no dynamic allocation. The benefit calculation is done once and the result (profit) is directly stored and used. - Second solution: Uses
vector<double>
as the heap element type. Storing heap elements asvector<double>
introduces unnecessary overhead. Vectors typically require dynamic allocations and memory management, resulting in increased overhead compared to fixed-size structures. Also, copying or movingvector<double>
objects is more expensive than dealing with a simple pair or array.
3. Repeated Floating-Point Operations in Comparisons
In the second solution, each comparison involves multiple floating-point divisions and subtractions to compute the incremental gain on the fly.
- Floating-point operations are more expensive than simple arithmetic on integers or precomputed doubles.
- Since the priority queue can make many comparisons (especially with large inputs), these repeated computations add significant runtime overhead.
In Summary:
Although the approach is the same, the second solution performs the gain calculation repeatedly within the comparator for each comparison in the priority queue, uses a heavier data structure type (vector<double>
rather than a small fixed-size structure), and thus incurs much more overhead. The first solution precomputes increments and stores them efficiently, avoiding repeated work and reducing memory allocation overhead. Hence, the first solution is significantly faster.