滑动窗口前 k 小数
c++
struct TopK {
int K;
// st1 保存前 K 小值 st2 保存其他值
multiset<LL> st1, st2;
// sum 维护前 st1 中元素和
LL sum;
TopK(int K): K(K), sum(0) {}
// 调整 st1 和 st2 的大小,保证调整后 st1 保存前 K 小值
void adjust()
{
while (st1.size() < K && st2.size() > 0)
{
LL t = *(st2.begin());
st1.insert(t);
sum += t;
st2.erase(st2.begin());
}
while (st1.size() > K)
{
LL t = *prev(st1.end());
st2.insert(t);
st1.erase(prev(st1.end()));
sum -= t;
}
}
// 插入
void add(LL x)
{
if (st2.size() && x >= *(st2.begin())) st2.insert(x);
else st1.insert(x), sum += x;
adjust();
}
// 删除
void del(LL x)
{
auto it = st1.find(x);
if (it != st1.end()) st1.erase(it), sum -= x;
else st2.erase(st2.find(x));
adjust();
}
};
例题参见 100178. 将数组分成最小总代价的子数组 II
对顶堆应用:求窗口内出现次数前 k 大的元素和
c++
using PII = pair<int, int>;
using LL = long long;
struct TopK {
int K;
// st1 保存前 K 小值 st2 保存其他值
multiset<PII> st1, st2;
// sum 维护前 st1 中元素和
LL sum;
TopK(int K): K(K), sum(0) {}
// 调整 st1 和 st2 的大小,保证调整后 st1 保存前 K 小值
void adjust()
{
while (st1.size() < K && st2.size() > 0)
{
PII t = *(st2.begin());
st1.insert(t);
sum += 1LL * t.first * t.second;
st2.erase(st2.begin());
}
while (st1.size() > K)
{
PII t = *prev(st1.end());
st2.insert(t);
st1.erase(prev(st1.end()));
sum -= 1LL * t.first * t.second;
}
}
// 插入
void add(PII x)
{
if (st2.size() && x >= *(st2.begin())) st2.insert(x);
else st1.insert(x), sum += 1LL * x.first * x.second;
adjust();
}
// 删除
void del(PII x)
{
auto it = st1.find(x);
if (it != st1.end()) st1.erase(it), sum -= 1LL * x.first * x.second;
else st2.erase(st2.find(x));
adjust();
}
};
class Solution {
public:
vector<long long> findXSum(vector<int>& nums, int k, int x) {
int n = nums.size();
vector<LL> res;
unordered_map<int, int> cnt;
TopK topk(x);
for (int i = 0; i < k; i++) {
cnt[nums[i]] ++;
}
// 因为模板维护的是前 x 小的元素,所以这里元素全部取反
for (auto &[x, c]: cnt) {
topk.add({-c, -x});
}
for (int i = 0; ; i++) {
res.push_back(topk.sum);
if (i + k == n) {
break;
}
topk.del({-cnt[nums[i]], -nums[i]});
cnt[nums[i]] --;
if (cnt[nums[i]] > 0) {
topk.add({-cnt[nums[i]], -nums[i]});
}
if (cnt[nums[i + k]] > 0) {
topk.del({-cnt[nums[i + k]], -nums[i + k]});
}
cnt[nums[i + k]] ++;
topk.add({-cnt[nums[i + k]], -nums[i + k]});
}
return res;
}
};
滑动窗口中位数
cpp
using LL = long long;
struct Median {
// st1:窗口内较小一半的数
// st2:窗口内较大一半的数
multiset<LL> st1, st2;
// st1 和 st2 的元素和
LL sum1, sum2;
Median() {
sum1 = sum2 = 0;
}
// 调整两个对顶堆,使得元素数量恰好是 floor(窗口长度 / 2) 以及 ceil(窗口长度 / 2)
void adjust() {
int tot = st1.size() + st2.size();
int a = tot / 2, b = tot - a;
while (st1.size() < a) {
LL t = *(st2.begin());
st1.insert(t);;
sum1 += t;
st2.erase(st2.begin());
sum2 -= t;
}
while (st2.size() < b) {
LL t = *prev(st1.end());
st2.insert(t);
sum2 += t;
st1.erase(prev(st1.end()));
sum1 -= t;
}
}
// 把 x 加入滑动窗口
void add(LL x) {
if (!st2.empty() && x >= *(st2.begin())) {
st2.insert(x);
sum2 += x;
} else {
st1.insert(x);
sum1 += x;
}
adjust();
}
// 从滑动窗口删除 x
void del(LL x) {
auto it = st1.find(x);
if (it != st1.end()) {
st1.erase(it);
sum1 -= x;
} else {
st2.erase(st2.find(x));
sum2 -= x;
}
adjust();
}
// 求窗口内所有数变得相等的最小代价
LL query() {
// 先求出中位数
LL mid = *(st2.begin());
// 再把所有数变成中位数
return (st1.size() * mid - sum1) + (sum2 - st2.size() * mid);
}
};
用处是可以动态求出把一个窗口内的数全变成一样的最小操作次数