对顶堆的理解与使用(直播获奖,中位数)

1.前言

在今天的做题过程中遇见了这样一类问题:
动态维护n个数中的 第k大的数 / 第k小的数 / 中位数
一开始没有什么思路,然后尝试每加入一个数用一次sort然后输出所要求的的数,不出意外爆掉了,在补题的时候看到了对顶堆这种非常优秀的数据结构,所以 学完写一篇博客,持续更新;

2.实现原理

我们用STL中的 单调队列大根堆和小根堆来实现对顶堆;

对顶堆由一个小根堆和一个大根堆组成,维护小根堆堆顶为第 k 大的元素,大于它的放在小根堆,小于它的放入大根堆

priority_queue< int > maxp;//大根堆
priority_queue<int,vector< int >,greater< int > >minp;//小根堆

在小根堆里维护小根堆堆顶为 第 k 大个元素
(维护中位数是维护第 k 大个元素的一个特例,维护第 k 小个元素即是维护第(n-k)大个元素)
请添加图片描述
1.元素的插入:
当我们有一个元素之后,我们是选择放入大根堆还是放入小根堆呢,我们在这里把小根堆堆顶作为一个标志(后面的实现会给出用大根堆堆顶做标志的不方便之处),大于它的放入小根堆,否则放入大根堆,等于它的放在哪里都可,因为对后面维护没影响,这样插入就保证了整个对顶堆的数据整体的有序性,一开始把第一个元素作为标志元素,方便维护;

void push(int num)
{
	if(num>=minp.top())//小根堆堆顶做标志
		minp.push(num);
	else 
		maxp.push(num);
		
	trans();//每插入一个数据维护一次
}

2.元素的维护
每次放入一个元素后我们都要做一次维护,维护很简单,因为整体元素已经有序,我们只要保证小根堆中的元素恰好为 k 个即可。少元素在大根堆堆顶取出,否则把自己堆顶元素放入大根堆

void trans()
{
	while(minp.size()<now)//now为k,即要维护的第k大,不够就去大根堆中取
	{
		minp.push(maxp.top());
		maxp.pop();
	} 
	while(minp.size()>now)//多了就往大根堆放
	{
		maxp.push(minp.top());
		minp.pop();
	}
} 

3.例题

1.洛谷P7072 直播获奖
题目大意:
根据题中给出人数与分数线的关系动态维护第 k 大的元素;

priority_queue<int> maxp;
priority_queue<int,vector<int>,greater<int>>minp;

int n,w,now,num;

void trans()//维护
{
	while(minp.size()<now)
	{
		minp.push(maxp.top());
		maxp.pop();
	} 
	while(minp.size()>now)
	{
		maxp.push(minp.top());
		minp.pop();
	}
	
} 

void push(int num)//插入
{
	if(num>=minp.top())
		minp.push(num);
	else 
		maxp.push(num);
		
	trans();
}

int main()
{
	scanf("%d %d",&n,&w);
	
	scanf("%d",&num);
	minp.push(num);
	printf("%d ",minp.top());//把第一个元素放在小根堆堆顶做标志元素,同时根据题意,当只有一个元素时第一个元素一定是分数线
	
	for (int p=2;p<=n;p++)
	{
		now=max(1,p*w/100);//整除就是小数的向下取整,不会出现误差
		scanf("%d",&num);
		push(num);
		printf("%d ",minp.top()); 
	}
	return 0;
}

2.洛谷P3871 中位数
题目大意:
动态维护中位数
根据题意,无论我们要维护的数总数为奇还是为偶
我们要的 k 总为( n / 2 ) + 1;

priority_queue<ll>maxq;
priority_queue<ll,vector<ll>,greater<ll>>minq;
ll n;
ll num,now;
ll m,cnt;
string s; 

void trans()//维护
{
	while(minq.size()>now)
	{
		maxq.push(minq.top());
		minq.pop();
	}
	while(minq.size()<now)
	{
		minq.push(maxq.top());
		maxq.pop();
	}
}

void push(int num)//插入
{
	if(num>=minq.top())
		minq.push(num);
	else
		maxq.push(num);
	trans();
}

int main()
{
	cin>>n;
	scanf("%lld",&num);
	minq.push(num);// n 必然大于 0 ,因此我们把第一个数作为标志元素
	
	for(int i=2;i<=n;i++)
	{
		scanf("%lld",&num);
		now=i/2+1;
		push(num);
	}//把 n 个元素插入并维护
	
	cin>>m;
	
	for(int i=1;i<=m;i++)
	{
		cin>>s;
		
		if(s=="add")
		{
			cnt++;
			now=(cnt+n)/2+1;
			scanf("%lld",&num);
			push(num);//记录新插入元素的个数,并维护对顶堆
		}
		else
		{
			printf("%lld\n",minq.top());//维护的小根堆的堆顶即为中位数
		}
	}
	return 0; 
} 

这里我们给出用大根堆堆顶作为标志的不方便之处
假如 n = 1;然后不插入直接查询,我们让大根堆堆顶作为标志的话,小根堆是空的最后还要特判,不如直接使用小根堆堆顶做插入标志方便,同时小根堆堆顶还是最后维护的值,比较好记;

4.拓展

当遇到不是动态维护第 k 大元素的时候,而仅仅是求一个无规则数列中的第 k 大的数时,我们可以借助STL中的 nth_element这个函数来实现

nth_element的作用是求第 k 小的元素,我们稍加改造就能求第 k 大的元素
它会使第 k 小的元素归位,使它前边的元素小于它,使它后面的元素大于它;
它的用法是
nth_element(a+x,a+x+y,a+x+len);

	能使下标从 x 到 x+y-1 的元素小于 x+y 

          从 x+y+1 到 x+y+len 的元素大于 x+y;

时间复杂度只有 O(n),十分好用

例题链接 洛谷P1923 求第k小的数

本人是新手,有什么错误欢迎指出~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

草莓猫猫软糖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值