上一次我讲了什么是堆以及堆的操作,相信大家都有所了解。今天我就来讲一讲堆的一些应用。
堆排序
题目描述
给你一串数,把他们排一个序。
思路
对于排序算法,想必大家都不陌生:选择排序,冒泡排序,快速排序等等。
不知道在大家未接触过排序算法时,有没有过这样的想法:第一次输出第一大(小)的,第二次输出第二大(小)的……第
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 n≤10000
思路
我们有一种贪心的想法,就是每次选最小的两个堆,然后重新放回去,然后再选最小的两个堆合并,知道只剩一个堆为止。
似乎有一种方法是先排序,合并完后二分把这个数插进去,不过我没实现过。
不理这一种方法,因为我们的标题是
↑
↑
↑ ,堆!
我们看我们说的思路,有“最小”这个词,重点在“最”!我们想一想堆支持什么(详见上一篇)?我们就可以知道,这一题可以用堆!所以我们可以每次取两次堆顶,然后合并、累加答案,之后把合并后的“堆”再塞进堆里面,直到堆里只剩下一个“堆”为止。
代码如下(仍然是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 1≤k≤(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,…,A2k−1 的中位数。即前 1 , 3 , 5 , … 1,3,5,… 1,3,5,… 个数的中位数。
数据范围
对于
20
%
20\%
20% 的数据,
N
≤
100
N ≤ 100
N≤100;
对于
40
%
40\%
40% 的数据,
N
≤
3000
N ≤ 3000
N≤3000;
对于
100
%
100\%
100% 的数据,
N
≤
100000
N ≤ 100000
N≤100000 。
思路
首先,我们要知道什么是中位数。
中位数(Median)又称中值,统计学中的专有名词,是按顺序排列的一组数据中居于中间位置的数,代表一个样本、种群或概率分布中的一个数值,其可将数值集合划分为相等的上下两部分。对于有限的数集,可以通过把所有观察值高低排序后找出正中间的一个作为中位数。如果观察值有偶数个,通常取最中间的两个数值的平均数作为中位数。
——选自“百度百科”
通俗点儿说,中位数就是排序后处于
⌈
n
+
1
2
⌉
⌈\frac{n+1}{2}⌉
⌈2n+1⌉ 的位置的那个数。那如果这个序列的个数为偶数怎么办?看
↑
↑
↑,不过,这道题也不用输出偶数的。
如果我们每次输入后排序,那肯定会TLE。
因此我们就想到了用堆来做。
怎么做呢?
我们可以维护两个堆,一个大根堆,一个小根堆。大根堆维护比中位数小的数,小根堆维护比中位数大的或等于的数,因此,我们要保证小根堆的元素个数等于大根堆的元素个数或比大根堆的元素个数多1。由此可得,中位数存在于小根堆的堆顶。
于是我们可以这么做:
每当读入一个数,判断如果小根堆元素如果大于大根堆元素,那么把这个元素放到大根堆中,否则放到小根堆中。
很显然,这种方法是不对的,有两种方法验证:
- 假如单单是这样弄,何必要弄堆呢?弄两个队列不行吗?
- 我们举个栗子,假如说现在两个堆的元素个数相等,大根堆的元素分别是: 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 是这一串数的中位数吗?很显然不是。若两个堆的元素不相等的话,同理,感兴趣的可以自己举例,不难举,嘿嘿。
所以,我们要在我们之前的想法中加上两条:
- 若想要加入到小根堆中,如果它小于大根堆堆顶,那么将大根堆堆顶弹出并放到小根堆中,再把这个数放到大根堆中,反之直接把它放到小根堆中;
- 若想要加入到大根堆中,如果它大于小根堆堆顶,那么将小根堆堆顶弹出并放到大根堆中,再把这个数放到小根堆中,反之直接把它放到大根堆中;
然后我们就开始打代码了(这次我倒用的是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!