文章目录
- 0. Leetcode [2099. 找到和最大的长度为 K 的子序列](https://leetcode.cn/problems/find-subsequence-of-length-k-with-the-largest-sum/)
- 1. Leetcode [1792. 最大平均通过率](https://leetcode.cn/problems/maximum-average-pass-ratio/)
- 2. Leetcode [1499. 满足不等式的最大值](https://leetcode.cn/problems/max-value-of-equation/)
- 3. Leetcode [2163. 删除元素后和的最小差值](https://leetcode.cn/problems/minimum-difference-in-sums-after-removal-of-elements/submissions/)
- 总结
优先队列(堆)的概念并不复杂,难点在于自己手撸一个堆。熟悉这个过程后,做题时 C++ 可以用
priority_queue<T>
。不熟悉的可以参考
这里。
0. Leetcode 2099. 找到和最大的长度为 K 的子序列
给你一个整数数组 nums 和一个整数 k 。你需要找到 nums 中长度为 k 的 子序列 ,且这个子序列的 和最大 。
请你返回 任意 一个长度为 k 的整数子序列。
子序列 定义为从一个数组里删除一些元素后,不改变剩下元素的顺序得到的数组。
分析与解答
可以将其当作排序,使用堆排序后,选出最大的 k k k 个。这里第一个堆手写一下:
class Solution {
int numIdx[2000];
unordered_map<int, int> id2Num;
int tail; // pos to insert new element
public:
vector<int> maxSubsequence(vector<int>& nums, int k) {
tail = 0;
// 构造大顶堆
for (int i = 0; i < nums.size(); ++i) {
id2Num[i] = nums[i];
numIdx[tail++] = i;
// 上浮
int curPos(tail - 1);
int prePos = (curPos - 1) / 2;
while (prePos >= 0) {
if (id2Num[numIdx[curPos]] > id2Num[numIdx[prePos]]) {
int tmp = numIdx[curPos];
numIdx[curPos] = numIdx[prePos];
numIdx[prePos] = tmp;
curPos = prePos;
prePos = (curPos - 1) / 2;
} else {
break;
}
}
}
// 获取前 k 个元素
vector<int> vec;
while (k--) {
vec.push_back(numIdx[0]);
numIdx[0] = numIdx[tail - 1];
// 下沉
int curPos(0);
int lIdx = curPos * 2 + 1;
while (lIdx < tail - 1) {
int sonIdx = lIdx;
int rIdx = curPos * 2 + 2;
if (id2Num[numIdx[sonIdx]] < id2Num[numIdx[rIdx]]) {
sonIdx = rIdx;
}
if (id2Num[numIdx[curPos]] < id2Num[numIdx[sonIdx]]) {
int curNum = numIdx[curPos];
numIdx[curPos] = numIdx[sonIdx];
numIdx[sonIdx] = curNum;
curPos = sonIdx;
lIdx = curPos * 2 + 1;
} else {
break;
}
}
tail--;
}
sort(vec.begin(), vec.end());
vector<int> result;
for (int i = 0; i < vec.size(); ++i) {
result.push_back(nums[vec[i]]);
}
return result;
}
};
我们也可以建立小顶堆,将前 k k k 个元素放入堆中后,搜索数组中剩下的元素。若当前元素比堆顶元素大,则删除堆顶元素,插入当前元素:
class Solution {
struct Data {
int idx;
int num;
bool operator<(const Data& o) const {
if (num == o.num) {
return idx < o.idx;
}
return num > o.num;
}
};
priority_queue<Data> q;
public:
vector<int> maxSubsequence(vector<int>& nums, int k) {
for (int i = 0; i < k; ++i) {
// 构建大小为 k 的堆
Data curData;
curData.idx = i;
curData.num = nums[i];
q.push(curData);
}
for (int i = k; i < nums.size(); ++i) {
// 确保堆中为 [0, i] 里 k 个最大的元素
Data curData = q.top();
if (curData.num < nums[i]) {
// 弹出堆中最小的元素,放入当前元素
q.pop();
Data cData;
cData.idx = i;
cData.num = nums[i];
q.push(cData);
}
}
// 获取下标并排序
vector<int> resultIdx;
while (!q.empty()) {
Data cur = q.top();
resultIdx.push_back(cur.idx);
q.pop();
}
// 根据下标取相应元素
sort(resultIdx.begin(), resultIdx.end());
vector<int> result;
for (int i = 0; i < resultIdx.size(); ++i) {
result.push_back(nums[resultIdx[i]]);
}
return result;
}
};
1. Leetcode 1792. 最大平均通过率
一所学校里有一些班级,每个班级里有一些学生,现在每个班都会进行一场期末考试。给你一个二维数组 classes ,其中 classes[i] = [passi, totali] ,表示你提前知道了第 i 个班级总共有 totali 个学生,其中只有 passi 个学生可以通过考试。
给你一个整数 extraStudents ,表示额外有 extraStudents 个聪明的学生,他们 一定 能通过任何班级的期末考。你需要给这 extraStudents 个学生每人都安排一个班级,使得 所有 班级的 平均 通过率 最大 。
一个班级的 通过率 等于这个班级通过考试的学生人数除以这个班级的总人数。平均通过率 是所有班级的通过率之和除以班级数目。
请你返回在安排这 extraStudents 个学生去对应班级后的 最大 平均通过率。与标准答案误差范围在 10-5 以内的结果都会视为正确结果。
分析与解答
本题关键在于明确优先队列中比较的 object
是什么。由于可以有
k
k
k 个必定及格的学生,因此每次分配一名学生到 当前增加一人后及格率变化最大的班级 即可,即:
m
a
x
i
x
i
+
1
y
i
+
1
−
x
i
y
i
max_i \frac{x_i + 1}{y_i + 1} - \frac{x_i}{y_i}
maxiyi+1xi+1−yixi
class Solution {
struct Pair {
int a;
int b;
Pair(int _a, int _b) {
a = _a;
b = _b;
}
void update() {
a++;
b++;
}
double getDelta() const {
return (double)(a + 1) / (b + 1) - (double)a / b;
}
// 注意插入时比较元素的 delta 值
bool operator < (const Pair& o) const {
return getDelta() < o.getDelta();
}
};
public:
double maxAverageRatio(vector<vector<int>>& classes, int extraStudents) {
priority_queue<Pair> q;
int n = classes.size();
for (int i = 0; i < n; ++i) {
q.push(Pair(classes[i][0], classes[i][1]));
}
while (extraStudents--) {
// 每次找到通过率变化最大的,插入
Pair curPair = q.top();
q.pop();
curPair.update();
q.push(curPair);
}
double result(0);
int num = q.size();
while (!q.empty()) {
result += (double)q.top().a / q.top().b;
q.pop();
}
return result / num;
}
};
2. Leetcode 1499. 满足不等式的最大值
给你一个数组 points 和一个整数 k 。数组中每个元素都表示二维平面上的点的坐标,并按照横坐标 x 的值从小到大排序。也就是说 points[i] = [xi, yi] ,并且在 1 <= i < j <= points.length 的前提下, xi < xj 总成立。
请你找出 yi + yj + |xi - xj| 的 最大值,其中 |xi - xj| <= k 且 1 <= i < j <= points.length。
题目测试数据保证至少存在一对能够满足 |xi - xj| <= k 的点。
分析与解答
这里需要做一下转换,由于 x i < x j x_i < x_j xi<xj,因此原式 y i + y j + ∣ x i − x j ∣ = y i + y j + x j − x i y_i + y_j + |x_i - x_j| = y_i + y_j + x_j - x_i yi+yj+∣xi−xj∣=yi+yj+xj−xi,由于对每个 j j j 而言, x j + y j x_j + y_j xj+yj 为定值,因此将问题转化为寻找 y i − x i y_i - x_i yi−xi 的最大值,且 x j − x i ≤ k x_j - x_i \leq k xj−xi≤k,很容易想到用堆求解:
class Solution {
struct Data {
int idx;
int val;
bool operator < (const Data& o) const {
if (val == o.val) {
return idx > o.idx;
}
return val < o.val;
}
};
priority_queue<Data> q; // 大顶堆
public:
int findMaxValueOfEquation(vector<vector<int>>& points, int k) {
int result(INT_MIN);
// y_i + y_j + x_j - x_i (x_i < x_j)
// 因此对每个 j,只要找到最大的 y_i - x_i 即可
int n = points.size();
for (int i = 0; i < n; ++i) {
int xi = points[i][0];
int yi = points[i][1];
int del = yi - xi;
// 寻找下标为 i 时最大结果
while (!q.empty()) {
Data topData = q.top();
if (xi - topData.idx > 0 && xi - topData.idx <= k) {
// 找到下标为 i 时最大结果,break
if (topData.val + xi + yi > result) {
result = topData.val + xi + yi;
}
break;
} else {
// 堆顶元素与 i 超过 k 则删除
while (xi - topData.idx > k && !q.empty()) {
q.pop();
topData = q.top();
}
}
}
Data tmp;
tmp.idx = xi;
tmp.val = del;
q.push(tmp); // 将当前元素插进堆中
}
return result;
}
};
3. Leetcode 2163. 删除元素后和的最小差值
给你一个下标从 0 开始的整数数组 nums ,它包含 3 * n 个元素。
你可以从 nums 中删除 恰好 n 个元素,剩下的 2 * n 个元素将会被分成两个 相同大小 的部分。
前面 n 个元素属于第一部分,它们的和记为 sumfirst 。
后面 n 个元素属于第二部分,它们的和记为 sumsecond 。
两部分和的 差值 记为 sumfirst - sumsecond 。
比方说,sumfirst = 3 且 sumsecond = 2 ,它们的差值为 1 。
再比方,sumfirst = 2 且 sumsecond = 3 ,它们的差值为 -1 。
请你返回删除 n 个元素之后,剩下两部分和的 差值的最小值 是多少。
分析与解答
本题先写出了一个超时答案,之后参考题解写出了正确解答…
先说思路,由于前
n
n
n 个元素必定在
s
u
m
f
i
r
s
t
sum_{first}
sumfirst 中,后
n
n
n 个元素必定在
s
u
m
s
e
c
o
n
d
sum_{second}
sumsecond 中,因此问题集中于中间
n
n
n 个元素的划分。对此可以遍历中间
n
n
n 个元素,以每个元素为切割点,将
[
0
,
n
+
i
]
[0, n + i]
[0,n+i] 划分为前半集合,
[
n
+
i
+
1
,
3
n
]
[n + i + 1, 3n]
[n+i+1,3n] 划分为后半集合,之后找出前半集合中
n
n
n 个最小元素;后半集合中
n
n
n 个最大元素即可得到题中所要求的差的最小值。
此处若直接遍历并构造两个优先队列会超时,因此使用前
n
n
n 个元素构造前半集合,使用后
n
n
n 个元素构造后半集合,之后从
[
n
,
2
n
]
[n, 2n]
[n,2n] 遍历,利用当前元素更新前半集合,保留
n
n
n 个最小元素;从
[
2
n
,
3
n
]
[2n, 3n]
[2n,3n] 进行逆序遍历,利用当前元素更新后半集合,保留
n
n
n 个最大元素:
class Solution {
int n2Del;
priority_queue<int, vector<int>, less<int>> q0; // 大顶堆
priority_queue<int, vector<int>, greater<int>> q1; // 小顶堆
public:
long long minimumDifference(vector<int>& nums) {
// 分成两个堆,分别找最大与最小;对中间的 n 进行循环
int n = nums.size();
n2Del = n / 3;
long long result(LONG_MAX);
// 前 n 个元素,每次删除最大的
// [0, n]; [0, n - 1]; ... [0, 2n]
vector<long long> numFirstVec;
long long numFirst(0);
for (int i = 0; i < n2Del; ++i) {
q0.push(nums[i]);
numFirst += nums[i];
}
numFirstVec.push_back(numFirst);
for (int i = n2Del; i < 2 * n2Del; ++i) {
// 循环剔除最大的元素
int cur = q0.top();
if (cur > nums[i]) {
q0.pop();
q0.push(nums[i]);
numFirst -= cur;
numFirst += nums[i];
}
numFirstVec.push_back(numFirst);
}
// 后 n 个元素,每次删除最小的
// [2n, 3n]; [2n - 1, 3n]; ... [n, 3n]
vector<long long> numSecondVec;
long long numSecond(0);
for (int i = 2 * n2Del; i < n; ++i) {
q1.push(nums[i]);
numSecond += nums[i];
}
numSecondVec.push_back(numSecond);
for (int i = 2 * n2Del - 1; i >= n2Del; --i) {
// 循环剔除最小的元素
int cur = q1.top();
if (cur < nums[i]) {
q1.pop();
q1.push(nums[i]);
numSecond -= cur;
numSecond += nums[i];
}
numSecondVec.push_back(numSecond);
}
// 注意这里要倒序后才是一一对应的,计算最小值
reverse(numSecondVec.begin(), numSecondVec.end());
for (int i = 0; i < numFirstVec.size(); ++i) {
if (result > numFirstVec[i] - numSecondVec[i]) {
result = numFirstVec[i] - numSecondVec[i];
}
}
return result;
}
};
总结
优先队列的概念很好理解,但在实际应用中,如何准确找到排序使用的指标十分重要。