算法笔记-堆
堆(
\rm Heap )是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。堆总是满足下列性质:
- 堆中某个结点的值总是不大于或不小于其父结点的值;
- 堆总是一棵完全二叉树。
上面提到的性质
如上是大根堆。
如上是小根堆。
堆实质上是一颗二叉树,二叉树的存储使用一维数组,编号为
因为性质
手写堆:
手写堆虽然麻烦,但是效率比较高,尽管有替代品但是这是理解堆及二叉树思想的最好方法,以下讲解为小根堆。
首先三个 #define
#define f(x) (((x)-1)/2)
#define l(x) ((x)*2+1)
#define r(x) ((x)*2+2)
三个宏定义分别表示
操作如下:
push向堆中压入一个元素pop(也有写法写作del(\tt delete ))从堆中弹出(删除)堆顶元素top查询堆顶元素
push:
压入一个元素,首先堆元素数量(大小)siz 需要
例如对此堆执行 push(0)
首先在堆底插入:
因为
因为
因为
push 函数为:
void push(int x) {
int pos = siz++;//在堆底插入
/*上调操作*/while (pos > 0) {
if (heap[f(pos)] <= x) {//如果父结点小于当前结点,满足性质
break;//停止
}
heap[pos] = heap[f(pos)], pos = f(pos);//互换位置
}
heap[pos] = x;//确定位置后赋值
return;
}
pop:
首先使用堆底结点替换掉堆顶元素(先删掉堆顶元素),然后对替换上来的结点不断下调直至满足性质。
我们对刚刚进行过 push 操作的堆进行 pop 操作:
执行 pop 使用
因为
因为
pop 函数为:
void pop() {
int pos = 0, x = heap[--siz];
while (l(pos) < siz) {
int t = (heap[l(pos)] < heap[r(pos)]) ? l(pos) : r(pos);//找较小儿子
/*下调操作*/if (heap[t] >= x) {
break;
}
heap[pos] = heap[t], pos = t;
}
heap[pos] = x;//同上
return;
}
top:
返回堆顶元素,也就是最小的元素。
执行 top ,返回顶端的
top 函数为:
int top() {
return heap[0];
}
代码:
此代码可过模板题
#include <bits/stdc++.h>
#define f(x) (((x)-1)/2)
#define l(x) ((x)*2+1)
#define r(x) ((x)*2+2)
#define JS ios::sync_with_stdio(false),cin.tie(nullptr),cout.tie(nullptr)
using namespace std;
int heap[1000001], siz;
void push(int x) {
int pos = siz++;
while (pos > 0) {
if (heap[f(pos)] <= x) {
break;
}
heap[pos] = heap[f(pos)], pos = f(pos);
}
heap[pos] = x;
return;
}
void pop() {
int pos = 0, x = heap[--siz];
while (l(pos) < siz) {
int t = (heap[l(pos)] < heap[r(pos)]) ? l(pos) : r(pos);
if (heap[t] >= x) {
break;
}
heap[pos] = heap[t], pos = t;
}
heap[pos] = x;
return;
}
int top() {
return heap[0];
}
int T, num, op;
int main() {
JS;
cin >> T;
while (T--) {
cin >> op;
if (op == 1) {
cin >> num;
push(num);
}
if (op == 2) {
cout << top() << '\n';
}
if (op == 3) {
pop();
}
}
return 0;
}
STL
STL 实现的堆有两种:
-
std::heap(堆) -
std::priority_queue(优先队列)
std::heap
在头文件 <algotithm> 中,使用 heap 需 include 或使用万能头。
make_heap():
先指定一个容器的范围建堆,常用 vector,建堆执行后最大值在所给范围的最左端,其他值并未排序。
使用方法:make_heap(v.begin(),v.end,_Comp/*自定义比较函数,非必要*/);v 代表 vector 的名称
v.push_back() 压入一个元素,但元素并未排序(没有进行上调操作)。
push_heap() 对刚刚压入元素的堆执行上调,用法:push_heap(v.begin(),v.end(),cmp/*自定义比较函数,非必要*/);
pop_heap() 等价于手写堆的 pop 弹出(删除)堆顶(最大值),用法:pop_heap(v.begin(),v.end(),cmp/*自定义比较函数,非必要*/);
sort_heap() 执行堆排序,可得到一个有序序列,用法:sort_heap(v.begin(),v.end(),cmp/*自定义比较函数,非必要*/);
std::priority_queue:
底层使用 std::heap 实现。
定义:priority_queue<T/*数据类型*/,Cotainer/*容器*/,Compare/*比较规则,可以使用自定义*/>
自带两种比较函数:
greater<T/*数据类型*/>小根堆less<T/*数据类型*/>大根堆
操作:
push() 压入元素,构造完毕后压入(执行上调)。
emplace()同时执行构造和压入(执行上调)。
pop() 弹出堆顶。
top() 返回堆顶元素。
size() 返回堆大小(元素个数)。
empty() 查询堆是否为空,返回值为 bool 类型,堆为空时返回 true,否则返回 false。
使用方法都为:q.成员函数() _q 代表 priority_queue 的名称_。
pb__ds
pb__ds (__gnu_pbds 命名空间里,使用其中的堆需要两个头文件:
#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/priority_queue.hpp>
万能头不能解决问题,<bits/stdc++.h> 只包含标准库的所有头文件,而pb__ds库是扩展库!
定义方法:
__gnu_pbds::priority_queue<T, Compare = std::less<T>, Tag = __gnu_pbds::pairing_heap_tag, Allocator = std::allocator<char>>
Tag 决定了堆的类型,共有五种堆:
__gnu_pbds::pairing_heap_tag配对堆push,pop效率较好,push,join复杂度为\mathcal{O}(1) ,但pop的最差复杂度很高。__gnu_pbds::binary_heap_tag二叉堆push,pop效率高。__gnu_pbds::binomial_heap_tag二项堆push,pop效率较差,但pop函数的时间复杂度最慢只有亚线性。__gnu_pbds::rc_binomial_heap_tag冗余计数二项堆 相比二项堆push,pop更差,但是push\mathcal{O}(1) 。__gnu_pbds::thin_heap_tag斐波那契堆(注:合并堆的复杂度与斐波那契堆不同)适合图论算法,在一些方面甚至优于斐波那契堆,不过由于封装过紧,常数大,反而在非图论算法上最劣。
操作:
push()压入元素。pop()弹出堆顶元素。top()返回堆顶元素。empty()返回是否为空(返回值bool类型)。size()返回堆大小。modify(iterator, key)修改迭代器位置的值。erase(iterator)删除迭代器位置的值。join()把另一个堆和当前堆合并并清空被合并的堆。
时间复杂度:
斐波那契堆:
斐波那契堆是堆的集合,比二叉堆拥有更高的效率,但是由于常数比较大而且不好写所以不常用。
堆优化 dijkstra:
每次更新最短距离的时候把对应的元素上调,每次从小根堆中取堆顶做下一次要用的顶点,从复杂度
复杂度:
除建堆和 top() 操作是
例题:
P3378 【模板】堆
P3377 【模板】左偏树(可并堆)
P1628 合并序列