1. 问题分析与解题思路拆解
这道题目要求我们计算一个工程团队的最大表现值,表现值的定义为团队中所有工程师的速度之和乘以团队中最低的效率值。我们需要从n个工程师中选出最多k个人组成团队,使得这个表现值最大化。
1.1 问题重述与理解
给定两个数组speed和efficiency,分别表示每个工程师的速度和效率。我们需要找到一个子集,满足:
- 子集大小不超过k
- 表现值 = sum(speed) * min(efficiency) 最大化
举个例子:
speed = [2,10,3,1,5,8]
efficiency = [5,4,3,9,7,2]
k = 2
可能的团队组合和表现值:
- 选第1和第2个工程师:(2+10)min(5,4) = 124 = 48
- 选第4和第5个工程师:(1+5)min(9,7) = 67 = 42
- 选第3和第5个工程师:(3+5)min(3,7) = 83 = 24
显然最大表现值是48。
1.2 解题思路分析
这道题的关键在于如何高效地枚举所有可能的团队组合,并计算它们的表现值。直接暴力枚举所有可能的组合显然不可行,因为组合数会随着n和k的增大而急剧增加。
我们需要找到一个更聪明的办法。观察到表现值由两部分决定:
- 速度之和:我们希望尽可能大
- 最低效率:我们希望尽可能大
这提示我们可以尝试固定其中一个变量,优化另一个变量。具体来说,我们可以:
- 按照效率从高到低排序
- 对于每个工程师,假设他是团队中效率最低的成员
- 在所有效率不低于他的工程师中,选择速度最快的k-1个与他组成团队
这样,我们就把问题分解为:
- 排序:O(nlogn)
- 对于每个工程师,维护一个大小为k的优先队列:O(nlogk)
总时间复杂度为O(nlogn + nlogk),这在n=1e5时是可接受的。
2. 算法实现细节解析
2.1 数据结构选择
为了实现上述思路,我们需要以下数据结构:
- 一个数组存储(efficiency, speed)对,并按效率降序排序
- 一个优先队列(最小堆)来维护当前最大的k个速度
优先队列选择最小堆的原因是:
- 我们需要快速访问当前k个速度中最小的那个
- 当有新工程师加入时,如果队列已满(k个),我们可以快速移除最小的速度,保持总和最大
2.2 算法步骤详解
让我们详细拆解代码中的每一步:
-
预处理阶段:
- 将efficiency和speed组合成pair,存入数组tr
- 按照efficiency降序排序这个数组
-
主循环阶段:
- 初始化一个空的最小堆pq和速度总和sum=0
- 对于排序后的数组中每个工程师(eff, spd):
a. 将他的speed加入堆中
b. 如果堆大小超过k,弹出最小的speed,并从sum中减去它
c. 将当前speed加入sum
d. 计算当前表现值 = sum * eff
e. 更新最大表现值
-
结果处理:
- 最后对最大表现值取模返回
2.3 代码逐行解析
cpp复制using pr = pair<int, int>;
class Solution {
public:
const int mod = 1e9 + 7; // 题目要求的模数
int maxPerformance(int n, vector<int>& speed, vector<int>& efficiency, int k) {
// 定义最小堆,比较函数是pair的第一个元素(speed)
priority_queue<pr, vector<pr>, decltype(greater<pr>())> pq;
vector<pr> tr; // 存储(efficiency, speed)对
// 组合efficiency和speed
for(int i = 0; i < n; i++)
tr.push_back({efficiency[i], speed[i]});
// 按efficiency降序排序
sort(tr.begin(), tr.end(), greater<pr>());
unsigned long long sum = 0, tmp = 0, now, cp, mx = 0;
for(int i = 0; i < n; i++) {
// 将当前speed加入堆中
pq.push({tr[i].second, tr[i].first});
// 如果堆大小超过k,移除最小的speed
if(pq.size() > k) {
sum -= pq.top().first;
pq.pop();
}
// 更新速度总和
sum += tr[i].second;
// 计算当前表现值
now = sum * tr[i].first;
// 更新最大表现值
if(mx < now) mx = now;
}
return mx % mod;
}
};
3. 关键点与优化技巧
3.1 为什么按效率降序排序
按效率降序排序后,当我们处理第i个工程师时,可以确保:
- 他是当前团队中效率最低的(因为后面的效率都比他低)
- 我们可以自由选择前面i-1个工程师中速度最快的k-1个与他组队
这样保证了我们总是固定当前的最低效率,然后最大化速度之和。
3.2 优先队列的使用技巧
这里使用最小堆来维护最大的k个速度,技巧在于:
- 堆的大小不超过k
- 当有新元素加入时,如果堆已满,就弹出最小的元素
- 这样堆中始终保留的是到目前为止最大的k个速度
这种技巧在解决"top k"类问题时非常常见,时间复杂度是O(nlogk)。
3.3 变量类型的选取
注意到表现值可能很大(sumspeed可以达到1e51e5*1e5=1e15),所以使用了unsigned long long来避免溢出。
4. 常见错误与调试技巧
4.1 容易犯的错误
-
排序方式错误:
- 错误地按照speed排序而不是efficiency
- 或者排序方向错误(升序而非降序)
-
优先队列比较函数错误:
- 错误地定义了最大堆而不是最小堆
- 比较了错误的pair元素(应该比较speed而不是efficiency)
-
变量溢出:
- 使用int而不是long long导致中间计算结果溢出
- 忘记在最后取模
-
边界条件处理:
- k=0或k>n的情况
- n=1的特殊情况
4.2 调试技巧
-
小样例测试:
构造小的测试用例,手动计算预期结果,与程序输出对比。例如:
speed = [2,10,3]
efficiency = [5,4,3]
k = 2
预期结果:max( (2+10)*4, (2+3)*5, (10+3)*3 ) = max(48,25,39) = 48 -
打印中间变量:
在循环中打印sum, now, mx等变量的值,观察是否符合预期。 -
检查排序结果:
确保排序后的数组确实是按efficiency降序排列的。
5. 算法复杂度分析
5.1 时间复杂度
- 排序阶段:O(nlogn)
- 主循环阶段:O(nlogk)(每个元素最多入堆出堆一次)
总时间复杂度:O(nlogn + nlogk) = O(nlogn) (因为logk <= logn)
5.2 空间复杂度
- 存储pair数组:O(n)
- 优先队列:O(k)
总空间复杂度:O(n + k) = O(n) (因为k <= n)
6. 类似题目与扩展思考
6.1 类似题目推荐
-
LeetCode 857. Minimum Cost to Hire K Workers:
类似的双因素优化问题,需要同时考虑工资和效率。 -
LeetCode 215. Kth Largest Element in an Array:
使用优先队列解决top k问题的经典题目。 -
LeetCode 253. Meeting Rooms II:
使用优先队列解决区间调度问题。
6.2 扩展思考
-
如果题目改为求"sum(speed) + min(efficiency)"的最大值,该如何解决?
- 这种情况下,我们需要完全不同的策略,因为两个因素现在是相加关系而非相乘。
-
如果k不是固定的,而是有一个范围[k1, k2],该如何解决?
- 可能需要维护一个滑动窗口或者更复杂的数据结构。
-
如果工程师之间有合作加成(即某些工程师组合会产生额外的表现值),该如何解决?
- 这会大大增加问题的复杂度,可能需要动态规划或其他高级算法。
7. 实际应用与工程实践
7.1 实际应用场景
这类问题在实际中有很多应用,例如:
- 团队组建:在资源有限的情况下,选择最优的团队成员组合
- 资源分配:选择最优的资源组合以最大化产出
- 投资组合:选择投资组合以最大化收益风险比
7.2 工程实践建议
-
代码可读性:
- 使用有意义的变量名(如用'efficiency'而不是'e')
- 添加适当的注释说明关键步骤
-
模运算的注意事项:
- 题目要求在最后取模,而不是在中间计算过程中
- 因为模运算会改变大小关系,可能影响比较结果
-
测试用例设计:
- 包括极端情况(n=1, k=1, k=n)
- 包括大数测试(验证是否溢出)
- 包括随机生成的测试用例
8. 个人实现心得
在实现这道题目时,我最初尝试了暴力枚举的方法,很快就发现这在n较大时不可行。通过分析问题特性,我意识到可以固定一个变量(效率)来优化另一个变量(速度之和),这大大降低了问题的复杂度。
使用优先队列来维护最大的k个速度是一个关键技巧,这在很多类似问题中都有应用。在实际编码中,我最初错误地定义了最大堆而不是最小堆,导致结果不正确。通过打印中间变量和手动计算小样例,我很快发现了这个问题。
另一个容易忽略的点是变量溢出。即使题目给出的输入在int范围内,中间计算结果(sum*efficiency)可能超出int范围。使用unsigned long long是一个安全的做法。