SKULL

一言(ヒトコト)

【寻迹#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;
} 

posted @ 2024-11-04 14:20  A&K.SKULL  阅读(12)  评论(0编辑  收藏  举报