一、堆的定义
堆(heap),是一棵有着特殊性质的完全二叉树,可以用来实现优先级队列(priority queue)。
堆需要满足以下性质:
1. 是一棵完全二叉树;
2. 对于树中每个结点,如果存在子树,那么该结点的权值大于等于(或小于等于)子树中所有结点
的权值。
如果根结点大于等
于子树结点的权值,称为大根堆;反之,称为小根堆。
二、堆的存储
由于堆是一个完全二叉树,因此我们可以用一个数组来存储。
当结点下标为 i 时:
如果父存在,父下标为 i/2 ;
如果左孩子存在,左孩子下标为 i × 2 ;
如果右孩子存在,右孩子下标为 i × 2 + 1 。
这样存储固然简单,但是题目不会那么好心,直接给我们一个标准的堆。一般给我们的是一组数,这组数按照给出的顺序还原成二叉树之后,并不是一个堆结构。此时如果想将这组数变成堆的话,有两种操作:
1. 用数组存下来这组数,然后把数组调整成一个堆;
2. 创建一个堆,然后将这组
数依次插入到堆中。
现在可能会有点迷糊,但看下去就会明白了。
三、
核心操作
堆中的所有运算,比如建堆,向堆中插入元素以及删除元素等,都是基于堆中的两个核心操作实现的——向上调整算法以及向下调整算法。因此,在实现堆之前,我们先来掌握两种核心操作。注意:以下所有操作都默认堆是一个大根堆,小根堆的原理反着来即可。
1.向上调整算法
算法流程:
(1)与父结点的权值作比较,如果比它大,就与父亲交换;
(2)交换完之后,重复 1 操作,直到比父亲小,或者换到根节点的位置。
#include <iostream>
using namespace std;
int n; // 标记堆的大小
int heap[N]; // 存堆 - 默认是大根堆
void up(int child)
{
int parent =child/2;
while(parent>=1&&heap[child]>heap[parent])
{
swap(heap[child],heap[parent]);
child=parent;
parent=child/2;
}
}
int main()
{
return 0;
}
2.向下调整算法
算法流程:
(1)找出左右儿子中权值最大的那个,如果比这个值小,就与其交换;
(2)交换完之后,重复 1 操作,直到比儿子结点的权值都大,或者换到叶节点的位置。
#include <iostream>
using namespace std;
int n; // 标记堆的大小
int heap[N]; // 存堆 - 默认是大根堆
void up(int child)
{
int parent =child/2;
while(parent>=1&&heap[child]>heap[parent])
{
swap(heap[child],heap[parent]);
child=parent;
parent=child/2;
}
}
void down(int parent)
{
int child=parent*2;
while(child<=n)
{
if(child+1<=n&&heap[child+1]>heap[child])child++;
if(heap[child]<=heap[parent])return;
swap(heap[child],heap[parent]);
parent=child;
child=parent*2;
}
}
int main()
{
return 0;
}
四、堆的模拟实现
1.创建
const int N=1e6+10;
int n; // 标记堆的大小
int heap[N]; // 存堆 - 默认是大根堆
2.插入
把新来的元素放在最后一个位置,然后从最后一个位置开始执行一次向上调整算法即可。
#include <iostream>
using namespace std;
const int N=1e6+10;
int n; // 标记堆的大小
int heap[N]; // 存堆 - 默认是大根堆
//向上调整算法
void up(int child)
{
int parent =child/2;
while(parent>=1&&heap[child]>heap[parent])
{
swap(heap[child],heap[parent]);
child=parent;
parent=child/2;
}
}
//向下调整算法
void down(int parent)
{
int child=parent*2;
while(child<=n)
{
if(child+1<=n&&heap[child+1]>heap[child])child++;
if(heap[child]<=heap[parent])return;
swap(heap[child],heap[parent]);
parent=child;
child=parent*2;
}
}
//插入
void push(int x)
{
heap[++n]=x;
up(n);
}
void
int main()
{
return 0;
}
3.删除栈顶元素
(1)将栈顶元素和最后一个元素交换,然后 n--,删除最后一个元素;
(2)从根节点开始执行一次向下调整算法即可。
#include <iostream>
using namespace std;
const int N=1e6+10;
int n; // 标记堆的大小
int heap[N]; // 存堆 - 默认是大根堆
//向上调整算法
void up(int child)
{
int parent =child/2;
while(parent>=1&&heap[child]>heap[parent])
{
swap(heap[child],heap[parent]);
child=parent;
parent=child/2;
}
}
//向下调整算法
void down(int parent)
{
int child=parent*2;
while(child<=n)
{
if(child+1<=n&&heap[child+1]>heap[child])child++;
if(heap[child]<=heap[parent])return;
swap(heap[child],heap[parent]);
parent=child;
child=parent*2;
}
}
//插入
void push(int x)
{
heap[++n]=x;
up(n);
}
//删除栈顶元素
void pop()
{
swap(heap[1],heap[n]);
n--;
down(1);
}
void
int main()
{
return 0;
}
4.堆顶元素
下标为 1 位置的元素,就是堆顶元素。
#include <iostream>
using namespace std;
const int N=1e6+10;
int n; // 标记堆的大小
int heap[N]; // 存堆 - 默认是大根堆
//向上调整算法
void up(int child)
{
int parent =child/2;
while(parent>=1&&heap[child]>heap[parent])
{
swap(heap[child],heap[parent]);
child=parent;
parent=child/2;
}
}
//向下调整算法
void down(int parent)
{
int child=parent*2;
while(child<=n)
{
if(child+1<=n&&heap[child+1]>heap[child])child++;
if(heap[child]<=heap[parent])return;
swap(heap[child],heap[parent]);
parent=child;
child=parent*2;
}
}
//插入
void push(int x)
{
heap[++n]=x;
up(n);
}
//删除栈顶元素
void pop()
{
swap(heap[1],heap[n]);
n--;
down(1);
}
//堆顶元素
int top()
{
return heap[1];
}
int main()
{
return 0;
}
5.堆的大小
#include <iostream>
using namespace std;
const int N=1e6+10;
int n; // 标记堆的大小
int heap[N]; // 存堆 - 默认是大根堆
//向上调整算法
void up(int child)
{
int parent =child/2;
while(parent>=1&&heap[child]>heap[parent])
{
swap(heap[child],heap[parent]);
child=parent;
parent=child/2;
}
}
//向下调整算法
void down(int parent)
{
int child=parent*2;
while(child<=n)
{
if(child+1<=n&&heap[child+1]>heap[child])child++;
if(heap[child]<=heap[parent])return;
swap(heap[child],heap[parent]);
parent=child;
child=parent*2;
}
}
//插入
void push(int x)
{
heap[++n]=x;
up(n);
}
//删除栈顶元素
void pop()
{
swap(heap[1],heap[n]);
n--;
down(1);
}
//堆顶元素
int top()
{
return heap[1];
}
//堆的大小
int size()
{
return n;
}
int main()
{
return 0;
}
五、priority_queue
1.优先级队列
普通的队列是一种先进先出的数据结构,即元素插入在队尾,而元素删除在队头。而在优先级队列中,元素被赋予优先级,当插入元素时,同样是在队尾,但是会根据优先级进行位置调整,优先级越高,调整后的位置越靠近队头;同样的,删除元素也是根据优先级进行,优先级最高的元素(队头)最先被删除。
其实我们可以认为,优先级队列就是堆实现的一个数据结构。priority_queue 就是 C++ 提供的,已经实现好的优先级队列,底层实现就是一个堆结构。在算法竞赛中,如果是需要使用堆的题目,我们一般就直接用现成的 priority_queue,很少手写一个堆,因为这样比较省事。
2.创建 priority_queue
优先级队列的创建结果有很多种,因为需要根据实际需求,可能会创建出来各种各样的堆:
(1)简单内置类型的大根堆或小根堆:比如存储 int 类型的大根堆或小根堆;
(2)存储字符串的大根堆或小根堆;
(3)存储自定义类型的大根堆或小根堆:比如堆里面的数据是一个结构体。
关于每⼀种创建结果,都需要有与之对应的写法。在初阶阶段,我们先用简单的 int 类型建堆,重点学习priority_queue 的用法。
注意: priority_queue 包含在 queue 这个头文件中。
我们先用
priority_queue来创建一个堆。
#include <iostream>
#include <queue>
using namespace std;
int main()
{
priority_queue<int> heap;// 默认写法下,是一个大根堆
int a[10]={ 1, 41, 23, 10, 11, 2, -1, 99, 14, 0};
return 0;
}
(1)size / empty
size :返回元素的个数。empty :返回优先级队列是否为空
。
(2)push
往优先级队列里面添加一个元素。
(3)pop
删除优先级最高的元素。
(4)top
获取优先级最高的元素。
#include <iostream>
#include <queue>
using namespace std;
int main()
{
priority_queue<int> heap;// 默认写法下,是一个大根堆
int a[10]={ 1, 41, 23, 10, 11, 2, -1, 99, 14, 0};
for(int i=0;i<10;i++)
{
heap.push(a[i]);
}
while(heap.size())
{
cout << heap.top() << " ";
heap.pop();
}
return 0;
}
从运行结果来看,为降序,也验证了我们创建出的是大根堆。
3.priority_queue的内置类型
内置类型就是 C++ 提供的数据类型,比如
int
、
double
、
long long
等。以
int
类型为例,分别创建大根堆和小根堆。
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
int main()
{
priority_queue<int> heap1;//默认就是大根堆
// priority_queue<数据类型, 存数据的结构, 数据之间的比较方式>
priority_queue<int, vector<int>, less<int>> heap2; //大根堆
priority_queue<int, vector<int>, greater<int>> heap3; // 小根堆
return 0;
}
4.priority_queue的结构体类型
当优先级队列里面存的是结构体类型时,需要在结构体中重载
< 比
较运算符,从而创建出大根堆
或者小根堆。
注:这里的主要目的是把优先级队列创建出来,因此只用掌握写法即可。关于其中背后的原理,这里就不再赘述,因为涉及比较复杂的 C++ 语法知识。在这里,学习的侧重点就是模仿。
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
struct node
{
int a, b, c;
// // 以 b 为基准,定义大根堆
// bool operator < (const node& x) const
// {
// return b < x.b;
// }
// 以 b 为基准,定义小根堆
bool operator < (const node& x) const
{
return b > x.b;
}
};
int main()
{
priority_queue<node> heap;
for(int i = 1; i <= 10; i++)
{
heap.push({i, i + 1, i + 2});
}
while(heap.size())
{
auto t = heap.top();
heap.pop();
cout << t.a << " " << t.b << " " << t.c << endl;
}
return 0;
}
在C++中,std::priority_queue默认是一个大根堆。这意味着如果没有自定义比较函数或重载operator<,std::priority_queue会将较大的元素视为更高的优先级。但因为我们已经通过重载operator<改变了比较规则,使得std::priority_queue基于自定义规则工作。在这个实现中,如果当前节点的b值大于另一个节点的b值,则认为当前节点“小于”另一个节点。这种逆向的比较逻辑意味着具有较小b值的节点会被赋予更高的优先级,因此它实际上表现得像一个小根堆。
六、相关的算法题
1.

#include <iostream>
#include <queue>
#include <vector>
using namespace std;
typedef long long LL;
priority_queue<LL,vector<LL>,greater<LL>> q;
int n,op;
LL x;
int main()
{
cin >> n;
while(n--)
{
cin >> op;
if(op==1)
{
cin >> x;
q.push(x);
}
else if(op==2)
{
cout << q.top() << endl;
}
else
{
q.pop();
}
}
return 0;
}
2.
#include <iostream>
#include <queue>
using namespace std;
int n, m, k, item, op;
priority_queue<int> q;
int main()
{
cin >> n >> m >> k;
while (n--)
{
cin >> item;
q.push(item);
if (q.size() > k)q.pop();
}
while (m--)
{
cin >> op;
if (op == 1)
{
cin >> item;
q.push(item);
if (q.size() > k)q.pop();
}
else
{
if (k > q.size())
cout << -1 << endl;
else
cout << q.top() << endl;
}
}
return 0;
}
3.
#include <iostream>
#include <queue>
using namespace std;
priority_queue<int> heap;
int i;
long long sum;
int main()
{
int n,k,item;
cin >> n >> k;
while(n--)
{
cin >> item;
heap.push(item);
}
while(heap.size()&&i<k)
{
if(heap.top()%2==1)
{
sum+=heap.top();
heap.pop();
}
else
{
int u=heap.top();
heap.pop();
heap.push(u/2);
i++;
}
}
int sz=heap.size();
while(sz--)
{
sum+=heap.top();
heap.pop();
}
cout << sum << endl;
return 0;
}
4.
#include <iostream>
#include <queue>
using namespace std;
const int N=1e4+10;
int a[N],b[N],c[N],sum;
struct node
{
int sum;//计算结果
int u;//第u组
int x;//x取值
bool operator<(const node& q) const
{
return sum>q.sum;
}
};
int calc(int l,int r)
{
return a[l]*r*r+b[l]*r+c[l];
}
priority_queue<node> heap;
int main()
{
int n,m;
cin >> n >> m;
for(int i=1;i<=n;i++)
{
cin >> a[i] >> b[i] >> c[i];
heap.push({calc(i,1),i,1});
}
while(m--)
{
auto v=heap.top();
cout << v.sum << " ";
int sum=v.sum;int u=v.u;int x=v.x;
heap.pop();
heap.push({calc(u,x+1),u,x+1});
}
return 0;
}
5.
#include <iostream>
#include <queue>
using namespace std;
const int N=1e5+10;
int a[N],b[N];
struct node
{
int sum;
int a;
int b;
bool operator<(const node& x) const
{
return sum>x.sum;
}
};
priority_queue<node> heap;
int main()
{
int n;
cin >> n;
for(int i=1;i<=n;i++)
{
cin >> a[i];
}
for(int i=1;i<=n;i++)
{
cin >> b[i];
}
for(int i=1;i<=n;i++)
{
heap.push({a[i]+b[1],i,1});
}
while(n--)
{
auto u=heap.top();
cout << u.sum << " ";
int sum=u.sum;int x=u.a;int y=u.b;
heap.pop();
heap.push({a[x]+b[y+1],x,y+1});
}
return 0;
}
6.
#include <iostream>
#include <queue>
#include <vector>
#include <cmath>
using namespace std;
struct node
{
int f;
int l;
int r;
bool operator<(const node& x) const
{
if(f != x.f) return f > x.f;
else if(l != x.l) return l > x.l;
else return r > x.r;
}
};
const int N=2e5+10;
int n;
char item;
int s[N];
int e[N],ne[N],pre[N];
priority_queue<node> heap;
vector<node> ret;
bool st[N];
int main()
{
cin >> n;
for(int i=1;i<=n;i++)
{
cin >> item;
if(item=='B')s[i]=1;
}
for(int i=1;i<=n;i++)
{
cin >> e[i];
pre[i]=i-1;
ne[i]=i+1;
}
ne[n]=0;
for(int i=2;i<=n;i++)
{
if(s[i]!=s[i-1])heap.push({abs(e[i]-e[i-1]),i-1,i});
}
while(heap.size())
{
node t = heap.top(); heap.pop();
int f = t.f, l = t.l, r = t.r;
if(st[l] || st[r]) continue;
ret.push_back(t);
st[l] = st[r] = true; // 标记 l 和 r 已经出列
// 修改指针,还原新的队列
ne[pre[l]] = ne[r];
pre[ne[r]] = pre[l];
// 判断新的左右是否会成为对
int left = pre[l], right = ne[r];
if(left && right && s[left] != s[right])
{
heap.push({abs(e[left] - e[right]), left, right});
}
}
cout << ret.size() << endl;
for(auto& x : ret)
{
cout << x.l << " " << x.r << endl;
}
return 0;
}