【寻迹#5】堆
堆
一、简介
1.结构
从二叉堆的结构说起,它是一棵二叉树,并且是完全二叉树,每个结点中存有一个元素(或者说,有个权值)。
堆性质:父亲的权值不小于儿子的权值(大根堆)。同样的,我们可以定义小根堆。
2.过程
(1)插入
插入操作是指向二叉堆中插入一个元素,要保证插入后也是一棵完全二叉树。
最简单的方法就是,最下一层最右边的叶子之后插入。
如果最下一层已满,就新增一层。
插入之后可能会不满足堆性质?
向上调整:如果这个结点的权值大于它父亲的权值,就交换,重复此过程直到不满足或者到根。
inline void Ins(int x)
{
Heap[++tot]=x;
up(tot);
}
可以证明,插入之后向上调整后,没有其他结点会不满足堆性质。
向上调整的时间复杂度是 \(O(\log n)\) 的。
inline void up(int x)
{
while(x>1)
{
if(Heap[x]<Heap[x/2]) { swap(Heap[x],Heap[x/2]);x>>=1; }
else break;
}
}
(2)删除
删除操作指删除堆中最大的元素,即删除根结点。
但是如果直接删除,则变成了两个堆,难以处理。
所以不妨考虑插入操作的逆过程,设法将根结点移到最后一个结点,然后直接删掉。
然而实际上不好做,我们通常采用的方法是,把根结点和最后一个结点直接交换。
于是直接删掉(在最后一个结点处的)根结点,但是新的根结点可能不满足堆性质……
向下调整:在该结点的儿子中,找一个最大的,与该结点交换,重复此过程直到底层。
inline void pop()
{
Heap[1]=Heap[tot--];
down(1);
}
可以证明,删除并向下调整后,没有其他结点不满足堆性质。
时间复杂度 \(O(\log n)\) 。
inline void down(int x)
{
int p=2*x;
while(p<=tot)
{
if(p<tot&&Heap[p]>Heap[p+1]) p++;
if(Heap[p]<Heap[x]) { swap(Heap[p],Heap[x]);x=p;p=2*x; }
else break;
}
}
(3)改变某个点的权值
比较显然,直接修改,然后向上或向下调整一次即可,时间复杂度 \(O(\log n)\) 。
二、题单
1.【模板】堆
思路:
模板题,试试水。
代码:
#include<bits/stdc++.h>
using namespace std;
#define N 1000050
int Heap[N];
int n,tot,op;
inline void up(int x)
{
while(x>1)
{
if(Heap[x]<Heap[x/2]) { swap(Heap[x],Heap[x/2]);x>>=1; }
else break;
}
}
inline void down(int x)
{
int p=2*x;
while(p<=tot)
{
if(p<tot&&Heap[p]>Heap[p+1]) p++;
if(Heap[p]<Heap[x]) { swap(Heap[p],Heap[x]);x=p;p=2*x; }
else break;
}
}
inline int Gettop() { return Heap[1]; }
inline void pop()
{
Heap[1]=Heap[tot--];
down(1);
}
inline void Ins(int x)
{
Heap[++tot]=x;
up(tot);
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
int x;
cin>>op;
if(op==2) cout<<Gettop()<<endl;
else if(op==3) pop();
else
{
cin>>x;
Ins(x);
}
}
return 0;
}
2.合并果子
思路:
经典题,用堆,直接用stl即可,注意stl默认大根堆,压到堆里的时候压负值即可。
每次取堆内的前两小元素合并,合并完了再压到堆里,
重复此操作,直到堆为空。
代码:
#include<iostream>
#include<queue>
#include<bits/stdc++.h>
using namespace std;
#define N 10050
int a[N],ans,n;
priority_queue<int>Q;
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
Q.push(-a[i]);
}
while(Q.size()>=2)
{
int x=-Q.top();Q.pop();
int y=-Q.top();Q.pop();
ans+=x+y;
Q.push(-(x+y));
}
cout<<ans<<endl;
return 0;
}
3.最小函数值
思路:
首先看到自变量的取值范围,发现 \(x\in\mathbb{N+}\) ,所以 \(x=2\) 时的函数值一定会比 \(x=1\) 时的函数值大。
先把所有函数在 \(x=1\) 时的函数值压进去,然后重复 \(m-1\) 次,每次取出堆顶最小的函数值并输出,然后将其 \(x+1\) 后的函数值计算出来压到堆里,
最后再输出一次堆顶的函数值即可。
代码:
#include<bits/stdc++.h>
using namespace std;
#define N 10050
int n,m;
int a[N],b[N],c[N],x[N];
priority_queue< pair<int,int> >Q;
inline int Cal(int i) { return a[i]*x[i]*x[i]+b[i]*x[i]+c[i]; }
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i]>>b[i]>>c[i];
for(int i=1;i<=n;i++)
{
x[i]=1;
Q.push(make_pair(-Cal(i),i));
}
for(int i=1;i<m;i++)
{
int p=-Q.top().first;int q=Q.top().second;
cout<<p<<" ";
x[q]++;
Q.pop();
Q.push(make_pair(-Cal(q),q));
}
cout<<-Q.top().first<<endl;
return 0;
}
4.中位数
思路:
从这道题开始就有点意思了,这里利用了堆的一个应用——对顶堆。
对顶堆是一组堆(两个堆),常用于解决求第 \(k\) 小的问题,
对顶堆包含两个堆,一个大根堆一个小根堆,
其中,大根堆存储小于中间值的数,小根堆存储大于中间值的数,
大根堆堆顶是小于中间值数里的最大值,小根堆顶是大于中间值的数的最小值 ,
对于这道题来说,当两个堆的大小相差 \(1\) 的时候,两个堆中元素数量较多的那个堆的堆顶元素就是我们要求的中位数,
先考虑怎么建堆,每次把大于上一个中位数的数压到小根堆中,把小于上一个中位数的数压到大根堆中,初始的时候中位数就是 \(a[1]\) ,
在考虑怎么维护对顶堆,在我们建完堆之后进行调整,每次取出数量较多的那个堆中的堆顶元素,将其压入另一个堆中,重复此操作,直到两个堆元素数量差 \(1\) 。
真正在操作中,中位数对应的值可能会反复出堆入堆,具体看代码实现。
代码:
#include<bits/stdc++.h>
using namespace std;
#define N 100050
int n,mid;
int a[N];
priority_queue<int>Q;//大根堆 (默认) 堆顶:小于中位数的最大值
priority_queue<int>H;//小根堆 堆顶:大于中位数的最小值
int main()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
cout<<a[1]<<endl;
mid=a[1];
for(int i=2;i<=n;i++)
{
if(a[i]>mid) H.push(-a[i]);
else Q.push(a[i]);
if(i%2)
{
while(Q.size()!=H.size())
{
if(Q.size()>H.size())
{
H.push(-mid);
mid=Q.top();
Q.pop();
}
else
{
Q.push(mid);
mid=-H.top();
H.pop();
}
}
cout<<mid<<endl;
}
}
return 0;
}
5.黑匣子
思路:
第 \(k\) 小问题,想到用对顶堆,
考虑一个堆里保证只有 \(k-1\) 个元素,则另一个堆的堆顶即为答案。
代码:
#include<bits/stdc++.h>
using namespace std;
#define N 200050
int n,m,a[N],u[N];
int mid,k,vis[N];
priority_queue<int>Q;//大根堆 维护小于mid的最大值
priority_queue<int>H;//小根堆 维护大于mid的最小值
int main()
{
cin>>m>>n;
for(int i=1;i<=m;i++) cin>>a[i];
for(int i=1;i<=n;i++) { cin>>u[i];vis[u[i]]++; }
for(int i=1;i<=n;i++)
{
while(k<u[i])
{
k++;
Q.push(a[k]);
H.push(-Q.top());
Q.pop();
}
cout<<-H.top()<<endl;
Q.push(-H.top());
H.pop();
}
return 0;
}
/*
Query:
7 4
3 1 -4 2 8 -1000 2
1 2 6 6
Ans:
3 3 1 2
这个写的确实巧妙,不妨这么来理解:压入堆中的时候,Q压进去一个就把top压到H里,保证Q个数不变,此时H堆顶为答案,输出答案后下一次查询时k要加1,所以把H的堆顶压到Q里,保证Q在下一轮个数是正确的。
*/
6.蚯蚓
思路:
一开始考虑用堆解决,不能暴力给每个蚯蚓加长度 \(q\) ,
用类似于线段树懒标记的方法,可以时间复杂度 \(O(m\log m)\) 通过。
接下来考虑更优的做法,考虑维护三个队列,
第一个队列是初始的蚯蚓,第二个队列是切下来的左端,第三个队列是切下来的右端。
现在我们将初始所有蚯蚓按降序排列后压入第一个队列,
那么我们可以发现,每次取出第一个队列的蚯蚓切完以后,放入二三队列后,二三队列也具有单调递减的特性,
所以我们每次取蚯蚓的时候,在三个队列的头中取一个最大的就好了。
代码:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
using namespace std;
#define N 100050
typedef long long ll;
ll n,m,q,u,v,t,a[N];
ll maxx,flag,l,r,cnt;
queue<ll>Q;queue<ll>L;queue<ll>R;
priority_queue<ll>H;
inline bool cmp(ll a,ll b) { return a>b; }
int main()
{
cin>>n>>m>>q>>u>>v>>t;
for(int i=1;i<=n;i++) cin>>a[i];
sort(a+1,a+1+n,cmp);
for(int i=1;i<=n;i++) Q.push(a[i]);
for(int i=1;i<=m;i++)
{
maxx=-0x3f3f3f3f;flag=0;
if(Q.size())
if(Q.front()>maxx) { maxx=Q.front();flag=1; }
if(L.size())
if(L.front()>maxx) { maxx=L.front();flag=2; }
if(R.size())
if(R.front()>maxx) { maxx=R.front();flag=3; }
if(flag==1) Q.pop();
else if(flag==2) L.pop();
else if(flag==3) R.pop();
maxx+=(i-1)*q;
l=maxx*u/v;r=maxx-l;
if(!(i%t)) cout<<maxx<<" ";
L.push(l-i*q);R.push(r-i*q);
}
puts("");
cnt=1;
while(cnt)
{
maxx=-0x3f3f3f3f;flag=0;
if(Q.empty()&&L.empty()&&R.empty()) break;
if(Q.size())
if(Q.front()>maxx) { maxx=Q.front();flag=1; }
if(L.size())
if(L.front()>maxx) { maxx=L.front();flag=2; }
if(R.size())
if(R.front()>maxx) { maxx=R.front();flag=3; }
if(flag==1) Q.pop();
else if(flag==2) L.pop();
else if(flag==3) R.pop();
if(!(cnt%t)) cout<<maxx+m*q<<" ";
cnt++;
}
return 0;
}