【LeetCode 每日一题】1792. 最大平均通过率——贪心 + 优先队列
Problem: 1792. 最大平均通过率
文章目录
- 整体思路
- 完整代码
- 时空复杂度
- 时间复杂度:O(N log N + K log N)
- 空间复杂度:O(N)
整体思路
这段代码旨在解决一个优化问题:给定一组班级,每个班级有 pass
个通过人数和 total
个总人数,以及 extraStudents
个额外的学生。你需要将这些额外的学生分配给各个班级,使得所有班级的平均通过率最大化。
该算法采用了一种 贪心策略 (Greedy Strategy),并利用 优先队列 (Priority Queue) 这一数据结构来高效地实现。其核心思想是:每一次分配,都把一个额外的学生分配给那个能带来最大通过率增益的班级。
-
定义“增益”:
- 算法的关键在于如何量化“增益”。如果一个班级当前有
p
个通过人数和t
个总人数,其通过率为p / t
。 - 如果我们给这个班级增加一个学生(假设这个学生也会通过,这是题目隐含的条件,为了最大化通过率),那么新的通过人数为
p+1
,总人数为t+1
,新的通过率为(p+1) / (t+1)
。 - 因此,增加一个学生带来的通过率增益为
(p+1)/(t+1) - p/t
。 - 我们的贪心策略就是,在每一步都选择使这个增益值最大的班级进行分配。
- 算法的关键在于如何量化“增益”。如果一个班级当前有
-
数据结构选择:优先队列
- 为了在每一步都能快速找到增益最大的班级,优先队列是理想的数据结构。
- 我们将所有班级放入一个**最大堆(Max-Heap)**中,堆的排序标准就是每个班级增加一个学生后所能带来的通过率增益。
-
排序标准的数学推导:
- 直接比较浮点数
(p+1)/(t+1) - p/t
可能会有精度问题,而且效率不高。 - 比较两个班级
a
和b
的增益,即比较(a_p+1)/(a_t+1) - a_p/a_t
和(b_p+1)/(b_t+1) - b_p/b_t
的大小。 - 通过通分和化简,增益
(p+1)/(t+1) - p/t
可以变为(t - p) / (t * (t+1))
。 - 所以,比较增益就等价于比较
(a_t - a_p) / (a_t * (a_t+1))
和(b_t - b_p) / (b_t * (b_t+1))
。 - 为了避免浮点数除法,我们可以进行交叉相乘:比较
(a_t - a_p) * b_t * (b_t+1)
和(b_t - b_p) * a_t * (a_t+1)
的大小。 - 这就是代码中
PriorityQueue
的Comparator
里的x
和y
的由来(其中a[0]
是pass
,a[1]
是total
)。 Long.compare(y, x)
实现了最大堆的效果:如果y
大于x
,则b
的增益大于a
,b
的优先级更高。
- 直接比较浮点数
-
算法执行流程:
- 初始化:创建一个根据上述增益公式排序的最大优先队列,并将所有初始班级信息加入队列。
- 贪心分配:循环
extraStudents
次。在每次循环中:
a. 从优先队列中取出poll()
增益最大的班级。
b. 将该班级的通过人数和总人数都加 1。
c. 将更新后的班级信息重新放回offer()
优先队列。由于其pass
和total
已经改变,它下次能带来的增益也会相应改变,队列会自动调整其位置。 - 计算最终结果:当所有额外的学生都分配完毕后,队列中存储的就是最优分配方案下所有班级的最终状态。
- 遍历队列,累加所有班级的最终通过率,然后除以班级总数,得到平均通过率。
完整代码
import java.util.PriorityQueue;class Solution {/*** 将 extraStudents 分配给各个班级,以最大化所有班级的平均通过率。* @param classes 一个二维数组,每个子数组 [pass, total] 代表一个班级的通过人数和总人数。* @param extraStudents 额外的学生数量。* @return 最大的平均通过率。*/public double maxAverageRatio(int[][] classes, int extraStudents) {// 创建一个最大优先队列。排序标准是:增加一个学生后,哪个班级的通过率增益最大。PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> {// a, b 分别是两个班级 [pass, total]// 增益公式 E(p,t) = (p+1)/(t+1) - p/t = (t-p) / (t*(t+1))// 比较 E(a) 和 E(b) 就是比较 (a_t-a_p)/(a_t*(a_t+1)) 和 (b_t-b_p)/(b_t*(b_t+1))// 为避免浮点数运算,交叉相乘比较:// (a_t-a_p) * b_t * (b_t+1) vs (b_t-b_p) * a_t * (a_t+1)// 使用 long 类型防止整数溢出long x = 1L * (a[1] - a[0]) * b[1] * (b[1] + 1);long y = 1L * (b[1] - b[0]) * a[1] * (a[1] + 1);// Long.compare(y, x) 实现最大堆:如果 y > x,则 b 的增益大,b 的优先级高。return Long.compare(y, x);});int n = classes.length;// 将所有初始班级加入优先队列for (int[] clazz : classes) {pq.add(clazz);}// 步骤 2: 贪心分配 extraStudentswhile (extraStudents-- > 0) {// 取出当前增益最大的班级int[] temp = pq.poll();// 分配一个学生给这个班级temp[0]++;temp[1]++;// 将更新后的班级放回队列,队列会自动重新排序pq.offer(temp);}// 步骤 3: 计算最终的平均通过率double ans = 0;while (!pq.isEmpty()) {int[] temp = pq.poll();// 累加每个班级的最终通过率ans += 1.0 * temp[0] / temp[1];}// 返回平均值return ans / n;}
}
时空复杂度
时间复杂度:O(N log N + K log N)
- 优先队列初始化:
- 将
N
个班级加入优先队列。每次add
操作的时间复杂度是 O(log N)。 - 因此,初始化总时间为 O(N log N)。
- 将
- 贪心分配循环:
while
循环执行K
次,其中K
是extraStudents
的数量。- 在每次循环中,执行一次
poll()
和一次offer()
操作。这两个操作的时间复杂度都是 O(log N),因为队列的大小始终是N
。 - 因此,这部分的总时间为 O(K log N)。
- 结果计算:
- 最后的
while
循环从队列中取出所有N
个元素,每次poll()
操作是 O(log N),总时间为 O(N log N)。
- 最后的
综合分析:
总的时间复杂度是 O(N log N) + O(K log N) + O(N log N) = O((N+K) log N)。
空间复杂度:O(N)
- 主要存储开销:算法使用了一个优先队列
pq
来存储所有的班级信息。 - 空间大小:
- 优先队列
pq
的大小始终是N
,即班级的总数。 - 每个元素是一个
int[2]
数组,占用的空间是常数。
- 优先队列
- 综合分析:
- 算法所需的额外空间主要由优先队列决定,其大小与班级的数量
N
成线性关系。 - 因此,空间复杂度为 O(N)。
- 算法所需的额外空间主要由优先队列决定,其大小与班级的数量