对顶堆
由一个大根堆和一个小根堆组成,小根堆里面的数永远比大根堆里面的数要大
用途:用于动态维护区间内第k大的数,要比线段树和动态平衡树写起来更简单
比如说我们要维护第k大的数,那么我们肯定是将前k大的数放进小根堆,小根堆的堆顶就是第k大的数
我们来看一下维护第k大的数的代码
1.建立大根堆和小根堆
priority_queue<int> qmax; //相当于大顶堆
priority_queue<int,vector<int>,greater<int>> qmin; //相当于小顶堆
2.如果小根堆是空的,或者说当前元素大于小根堆的堆顶元素,那么就将当前元素存入小根堆,否则就存入大根堆
if(qmin.empty()||a>=qmin.top())
{
qmin.push(a);
}
else
{
qmax.push(a);
}
3.大、小根堆的维护,当我们小根堆的元素,不满k个,就将大根堆的堆顶元素弹入小根堆,如果我们当前小根堆的元素超过了k个,就将小根堆的堆顶元素摊入大根堆
while(qmin.size()>k)
{
qmax.push(qmin.top());
qmin.pop();
}
while(qmin.size()<k&&!qmax.empty())
{
qmin.push(qmax.top());
qmax.pop();
}
4.查询第k大的元素,访问小根堆的堆顶元素
cout<<qmin.top()<<"\n";
例题
P1168 中位数
思路:去求奇数项的中位数,这个中位数的下标是在不断变化的,很板的一种动态维护第k大的元素的板题,直接套用对顶堆代码即可
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,k;
int a;
priority_queue<int> qmax;
priority_queue<int,vector<int>,greater<int> > qmin;
signed main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a;
if(i%2==1)
{
k=(i+1)/2;
}
if(qmin.empty()||a>=qmin.top())
{
qmin.push(a);
}
else
{
qmax.push(a);
}
while(qmin.size()>k)
{
qmax.push(qmin.top());
qmin.pop();
}
while(qmin.size()<k&&!qmax.empty())
{
qmin.push(qmax.top());
qmax.pop();
}
if(i%2==1)
{
cout<<qmin.top()<<"\n";
}
}
return 0;
}
P7072 [CSP-J2020] 直播获奖
思路:第一眼看上去,获奖分数线为多少?即i*w/100位置上的人的分数是多少,翻译过来还是第k大的元素是多少,动态维护第k大的对顶堆板题
直接写就行
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,k;
double w;
int a;
priority_queue<int>qmax;
priority_queue<int,vector<int>,greater<int> >qmin;
signed main()
{
cin>>n>>w;
for(int i=1;i<=n;i++)
{
cin>>a;
int z=floor(i*w/100);
k=max(1LL,z);
if(qmin.empty()||a>=qmin.top())
{
qmin.push(a);
}
else
{
qmax.push(a);
}
while(qmin.size()<k&&!qmax.empty())
{
qmin.push(qmax.top());
qmax.pop();
}
while(qmin.size()>k)
{
qmax.push(qmin.top());
qmin.pop();
}
cout<<qmin.top()<<" ";
}
return 0;
}
P2085 最小函数值
思路:我们发现a,b,c都是正数,那么其堆成周-b/2*a,永远都在负数的位置,也就是说,这n个函数,在0~正无穷内都是单调递增的,那么我们可以考虑一种剪枝的O(n*m)的写法,我们首先创建一个大顶堆,存储前k小的元素,我们在最外层遍历n个函数,内层遍历m个数,当n为1的时候,先将m个函数值存进去,然后当后续的函数,如果存在比堆顶元素小的,就存进大顶堆,弹出堆顶
,如果比堆顶元素大,那就立刻结束当前内层循环,因为后面也会比堆顶元素大,。
直到双重循环遍历完,然后输出倒序前m个即可
//P2085 最小函数值
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,m;
priority_queue<int>qmax;
int a,b,c;
int f;
int ans[200005];
signed main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>a>>b>>c;
for(int j=1;j<=m;j++)
{
f=a*j*j+b*j+c;
if(i==1)
{
qmax.push(f);
}
else
{
if(f<qmax.top())
{
qmax.pop();
qmax.push(f);
}
else
{
break;
}
}
}
}
for(int i=1;i<=m;i++)
{
ans[i]=qmax.top();
qmax.pop();
}
for(int i=m;i>=1;i--)
{
cout<<ans[i]<<" ";
}
return 0;
}
P1631 序列合并
思路:和上面那个一个思路,我们写一个有剪枝的O(n^2)的写法 ,和上面那个思路差不多
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n;
int a[100005];
int b[100005];
int ans[100005];
priority_queue<int> q;
signed main()
{
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
for(int i=1;i<=n;i++)
cin>>b[i];
sort(a+1,a+1+n);
sort(b+1,b+1+n);
int k;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
k=a[i]+b[j];
if(i==1)
{
q.push(k);
}
else
{
if(k<q.top())
{
q.pop();
q.push(k);
}
else
{
break;
}
}
}
}
for(int i=1;i<=n;i++)
{
ans[i]+=q.top();
q.pop();
}
for(int i=n;i>=1;i--)
cout<<ans[i]<<" ";
return 0;
}
P1801 黑匣子
思路:我们很容易发现,题目还是动态求第k小的,因此我们要将前k小的,放入大根堆中,当我们大根堆位空,或者说当前元素小于大根堆的堆顶,就存进来,我们用vis[i]去统计要进行输出的位置即可
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,m;
int a[200005];
int b[200005];
int vis[200005];
priority_queue<int> qmax;
priority_queue<int,vector<int>,greater<int> > qmin;
signed main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
for(int i=1;i<=m;i++)
{
cin>>b[i];
vis[b[i]]++;
}
int k=0;
for(int i=1;i<=n;i++)
{
if(qmax.empty()||a[i]<qmax.top())
{
qmax.push(a[i]);
}
else
{
qmin.push(a[i]);
}
while(vis[i])
{
k++;
while(qmax.size()<k)
{
qmax.push(qmin.top());
qmin.pop();
}
while(qmax.size()>k)
{
qmin.push(qmax.top());
qmax.pop();
}
cout<<qmax.top()<<"\n";
vis[i]--;
}
}
return 0;
}