堆(二)堆的应用

上一次我讲了什么是堆以及堆的操作,相信大家都有所了解。今天我就来讲一讲堆的一些应用。

堆排序

题目描述

给你一串数,把他们排一个序。

思路

对于排序算法,想必大家都不陌生:选择排序,冒泡排序,快速排序等等。
不知道在大家未接触过排序算法时,有没有过这样的想法:第一次输出第一大(小)的,第二次输出第二大(小)的……第 n n n次输出第 n n n 大(小)的。
事实上,这一种想法就是堆排序的主要思想:首先建一个堆,然后每次取出堆顶并输出,然后删除堆顶(也叫弹出堆顶)。
代码如下(请允许我用Pascal,因为我不想再打一遍了,不过大家也应该看得懂):

//顺便吐槽一下CSDN竟然插入不了Pascal的程序。
var
        n,num,i:longint;
        a:array[0..1000005] of longint;
procedure down(x:longint);//下移x这个位置的数
var
        y,t:longint;
begin
        while (2*x<=num) and (a[x]>a[2*x]) or (2*x+1<=num) and (a[x]>=a[2*x+1]) do
        begin
                y:=2*x;
                if (y+1<=num) and (a[y+1]<a[y]) then inc(y);
                t:=a[x];
                a[x]:=a[y];
                a[y]:=t;
                x:=y;
        end;
end;
procedure delete(x:longint);//删除x这个位置的数
begin
        a[x]:=a[num];
        dec(num);
        down(x);
end;
begin
        readln(n);
        for i:=1 to n do
                readln(a[i]);
        num:=n;
        for i:=n div 2 downto 1 do//建堆
                down(i);
        for i:=1 to n do
        begin
                writeln(a[1]);
                delete(1);
        end;
end.

时间复杂度(较稳定): O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)

合并果子,见洛谷P1090

题目描述
每一次合并,多多可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。可以看出,所有的果子经过n-1次合并之后,就只剩下一堆了。多多在合并果子时总共消耗的体力等于每次合并所耗体力之和。
举个栗子,有3种果子,数目依次为1,2,9。可以先将1、2堆合并,新堆数目为3,耗费体力为3。接着,将新堆与原先的第三堆合并,又得到新的堆,数目为12,耗费体力为12。所以多多总共耗费体力=3+12=15。可以证明15为最小的体力耗费值。
数据范围

n ≤ 10000 n≤10000 n10000

思路

我们有一种贪心的想法,就是每次选最小的两个堆,然后重新放回去,然后再选最小的两个堆合并,知道只剩一个堆为止。
似乎有一种方法是先排序,合并完后二分把这个数插进去,不过我没实现过。
不理这一种方法,因为我们的标题是 ↑ ↑ ,堆!
我们看我们说的思路,有“最小”这个词,重点在“最”!我们想一想堆支持什么(详见上一篇)?我们就可以知道,这一题可以用堆!所以我们可以每次取两次堆顶,然后合并、累加答案,之后把合并后的“堆”再塞进堆里面,直到堆里只剩下一个“堆”为止。
代码如下(仍然是Pascal):

//那时候我打的程序跟现在的方法有些不同,大家谅解一下
var
        n,i,num,f:longint;
        h:array[0..10005] of int64;
        ans:int64;
procedure swap(x,y:longint);//交换
var
        t:longint;
begin
        t:=h[x];
        h[x]:=h[y];
        h[y]:=t;
end;
procedure down(i:longint);//上移i这个位置的数
var
        t:longint;
begin
        while (i*2<=n) do
        begin
                if h[i]>h[i*2] then
                        t:=i*2
                else
                        t:=i;
                if i*2+1<=n then
                begin
                        if h[t]>h[i*2+1] then
                                t:=i*2+1;
                end;
                swap(t,i);
                if i=t then exit;
				i:=t;
        end;
end;
procedure up(i:longint);//上移i这个位置的数
begin
		while i>1 do
		begin
			if h[i]<h[i div 2] then
			swap(i,i div 2);
			i:=i div 2;
		end;
end;
begin
    	readln(num);
    	for i:=1 to num do
    	begin
            	read(h[i]);
            	up(i);
    	end;
		n:=num;
    	ans:=0;
		for i:=1 to num-1 do
		begin
	        	f:=h[1];
            	h[1]:=h[n];
            	dec(n);//相当于n--
             	down(1);
                ans:=ans+h[1]+f;//累加答案
                h[1]:=h[1]+f;
                down(1);
		end;
		write(ans);
end.

中位数(较难),见洛谷P1168

题目描述

给出一个长度为 N N N 的非负整数序列 A i A_i Ai,对于所有 1 ≤ k ≤ ( N + 1 ) / 2 1 ≤ k ≤ (N + 1) / 2 1k(N+1)/2 ,输出 A 1 , A 3 , A 5 , … , A 2 k − 1 ​ A_1,A_3,A_5,…,A _{ 2k−1​} A1,A3,A5,,A2k1 的中位数。即前 1 , 3 , 5 , … 1,3,5,… 1,3,5, 个数的中位数。

数据范围

对于 20 % 20\% 20% 的数据, N ≤ 100 N ≤ 100 N100
对于 40 % 40\% 40% 的数据, N ≤ 3000 N ≤ 3000 N3000
对于 100 % 100\% 100% 的数据, N ≤ 100000 N ≤ 100000 N100000

思路

首先,我们要知道什么是中位数。

中位数(Median)又称中值,统计学中的专有名词,是按顺序排列的一组数据中居于中间位置的数,代表一个样本、种群或概率分布中的一个数值,其可将数值集合划分为相等的上下两部分。对于有限的数集,可以通过把所有观察值高低排序后找出正中间的一个作为中位数。如果观察值有偶数个,通常取最中间的两个数值的平均数作为中位数。
——选自“百度百科”

通俗点儿说,中位数就是排序后处于 ⌈ n + 1 2 ⌉ ⌈\frac{n+1}{2}⌉ 2n+1 的位置的那个数。那如果这个序列的个数为偶数怎么办?看 ↑ ↑ ,不过,这道题也不用输出偶数的。
如果我们每次输入后排序,那肯定会TLE。
因此我们就想到了用堆来做。
怎么做呢?
我们可以维护两个堆,一个大根堆,一个小根堆。大根堆维护比中位数小的数,小根堆维护比中位数大的或等于的数,因此,我们要保证小根堆的元素个数等于大根堆的元素个数或比大根堆的元素个数多1。由此可得,中位数存在于小根堆的堆顶。
于是我们可以这么做:
每当读入一个数,判断如果小根堆元素如果大于大根堆元素,那么把这个元素放到大根堆中,否则放到小根堆中。
很显然,这种方法是不对的,有两种方法验证:

  1. 假如单单是这样弄,何必要弄堆呢?弄两个队列不行吗?
  2. 我们举个栗子,假如说现在两个堆的元素个数相等,大根堆的元素分别是: 4 , 1 , 2 4,1,2 4,1,2,而小根堆的元素分别是: 5 , 6 , 7 5,6,7 5,6,7,现在我们要插入 3 3 3,因为两个堆的元素相等,所以我们应该把它塞到小根堆里面,之后,小根堆里的元素分别是: 3 , 5 , 6 , 7 3,5,6,7 3,5,6,7,然而, 3 3 3 是这一串数的中位数吗?很显然不是。若两个堆的元素不相等的话,同理,感兴趣的可以自己举例,不难举,嘿嘿。

所以,我们要在我们之前的想法中加上两条:

  1. 若想要加入到小根堆中,如果它小于大根堆堆顶,那么将大根堆堆顶弹出并放到小根堆中,再把这个数放到大根堆中,反之直接把它放到小根堆中;
  2. 若想要加入到大根堆中,如果它大于小根堆堆顶,那么将小根堆堆顶弹出并放到大根堆中,再把这个数放到小根堆中,反之直接把它放到大根堆中;

然后我们就开始打代码了(这次我倒用的是C++,不过没有手打堆,而是用了优先队列,不懂的上网查,我就不详细讲了,嘿嘿):

//这里不好解释,不会优先队列的自己上网查
//建议把手打堆打熟练后再打优先队列
#include<queue>
#include<cstdio>
using namespace std;
int n,x;
priority_queue<int>s;
priority_queue<int>b;
int main(){
    scanf("%d",&n);
    scanf("%d",&x);
    printf("%d\n",x);
    s.push(-x);
    scanf("%d",&x);
    if (x>-s.top()){
        b.push(-s.top());
        s.pop();
        s.push(-x);
    }
    else
        b.push(x);
    for (int i=3;i<=n;i++){
        scanf("%d",&x);
        if (s.size()>b.size()){
            if (x>-s.top()){
                b.push(-s.top());
                s.pop();
                s.push(-x);
            }
            else
                b.push(x);
        }
        else{
            if (x<b.top()){
                s.push(-b.top());
                b.pop();
                b.push(x);
            }
            else
                s.push(-x);
        }
        if (i&1) printf("%d\n",-s.top());
    }
    return 0;
}

总结

这次就讲到这里了,如果你喜欢我的文章,那就点赞吧!
关于堆的我也讲完了,如果我有什么地方没有讲好,欢迎大家提出!
哦,别忘了我的blog!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值