堆是好东西啊!
你看,代码可短可短了:
#include<iostream>
#include<cstdlib>
#include<vector>
using namespace std;
template <typename Val>
class heap{
public:
heap(){size_h=0;}
void push(Val x){size_h++;while(v.size()<=size_h)v.push_back(0);v[size_h]=x;up(size_h);}
void pop(){_swap(v[1],v[size_h]),size_h--,down(1);}
Val top(){return v[1];}
int size(){return size_h;}
void print(){for(auto i:v)cout<<i<<' ';cout<<endl;}
private:
vector<Val>v;int size_h;
void _swap(Val &a,Val &b){Val tmp=a;a=b;b=tmp;}
void up(int x){if(x==1)return;if(v[x>>1]<v[x])_swap(v[x>>1],v[x]),up(x>>1);}
void down(int x){
int son=x<<1;
if(son>size_h)return;else if(son<size_h && v[son]<v[son+1])son++;
if(v[x]<v[son])_swap(v[son],v[x]),down(son);
return;
}
};
struct node{
int val;
node(){}
node(int x){val=x;}
bool operator<(const node &x)const{
return val>x.val;
}
};
heap<node>a;
int n;
int main(){
srand(time(0));
cin>>n;
int t;
for(int i=0;i<n;i++)cin>>t,a.push(node(t));
for(int i=0;i<n;i++,a.pop())cout<<a.top().val<<' ';
return 0;
}
我保证没压行((((((((
更重要的是,它比快排好写多了啊啊啊啊啊啊!!!
好吧我承认它其实本来长这样:
#include <iostream>
#include <cstdlib>
#include <vector>
using namespace std;
template <typename Val>
class heap
{
public:
heap() { size_h = 0; }
void push(Val x)
{
size_h++;
while (v.size() <= size_h)
v.push_back(0);
v[size_h] = x;
up(size_h);
}
void pop() { _swap(v[1], v[size_h]), size_h--, down(1); }
Val top() { return v[1]; }
int size() { return size_h; }
void print()
{
for (auto i : v)
cout << i << ' ';
cout << endl;
}
private:
vector<Val> v;
int size_h;
void _swap(Val &a, Val &b)
{
Val tmp = a;
a = b;
b = tmp;
}
void up(int x)
{
if (x == 1)
return;
if (v[x >> 1] < v[x])
_swap(v[x >> 1], v[x]), up(x >> 1);
}
void down(int x)
{
int son = x << 1;
if (son > size_h)
return;
else if (son < size_h && v[son] < v[son + 1])
son++;
if (v[x] < v[son])
_swap(v[son], v[x]), down(son);
return;
}
};
struct node
{
int val;
node() {}
node(int x) { val = x; }
bool operator<(const node &x) const
{
return val > x.val;
}
};
heap<node> a;
int n;
int main()
{
srand(time(0));
cin >> n;
int t;
for (int i = 0; i < n; i++)
cin >> t, a.push(node(t));
for (int i = 0; i < n; i++, a.pop())
cout << a.top().val << ' ';
return 0;
}
但其实,这才是最开始的代码:
#include<iostream>
#include<cstdlib>
#include<vector>
using namespace std;
template <typename Val>
class heap{
public:
heap(){size_h=0;}
void push(Val x){
size_h++;
while(v.size()<=size_h)v.push_back(0);
v[size_h]=x;
up(size_h);
}
void pop(){
_swap(v[1],v[size_h]);
size_h--;
down(1);
}
Val top(){
return v[1];
}
int size(){
return size_h;
}
void print(){
for(auto i:v)cout<<i<<' ';
cout<<endl;
}
private:
vector<Val>v;
int size_h;
void _swap(Val &a,Val &b){
Val tmp=a;
a=b;
b=tmp;
}
int ls(int x){
return x<<1;
}
int rs(int x){
return (x<<1)+1;
}
int fa(int x){
return x>>1;
}
void up(int x){
if(x==1)return;
if(v[fa(x)]<v[x]){
_swap(v[fa(x)],v[x]);
up(fa(x));
}
return;
}
void down(int x){
if(ls(x)>size_h)return;
if(rs(x)>size_h){
if(v[x]<v[ls(x)]){
_swap(v[ls(x)],v[x]);
down(ls(x));
}
}
else {
if(v[ls(x)]<v[rs(x)]){
if(v[x]<v[rs(x)]){
_swap(v[rs(x)],v[x]);
down(rs(x));
}
}
else{
if(v[x]<v[ls(x)]){
_swap(v[ls(x)],v[x]);
down(ls(x));
}
}
}
return;
}
};
struct node{
int val;
node(){}
node(int x){val=x;}
bool operator<(const node &x)const{
return val>x.val;
}
};
int n;
heap<node>a;
int main(){
srand(time(0));
cin>>n;
int t;
for(int i=0;i<n;i++){
cin>>t;
a.push(node(t));
}
for(int i=0;i<n;i++){
cout<<a.top().val<<' ';
a.pop();
}
return 0;
}
堆听起来码量很大,其实你看压完行了并不多。
以上代码,只需要一个STD:priority_queue
手打显然方便理解。
解决问题:如何动态维护一个集合的最大值,包括插入任意元素与查询并删除最大元素?
我们考虑朴素的$O(n^2)$做法:插入排序。
因为有删除操作,如果只维护最大值,删除操作就是$O(n)$的;
因为有插入操作,如果维护完整排序,插入操作就是$O(n)$的。
有人会不会想到用二分去快速查找相应位置?
很可惜,二分不能支持链表操作。插入排序的swap方法依然会将复杂度退化到$O(n)$。
这叫做复杂度不平衡,如果一种操作过多就会导致复杂度爆炸。
当我们将两个操作都改为$O(\log n)$的,这个操作就叫复杂度均摊。(这也是OI的常用技巧)
考虑这样一件事,每次我删去最大值后,取而代之的就是次最大值,有没有办法去很快的维护这个次最大值呢?
我们要是每次以第一个插入的数为标准,把接下来要插入的数划分成比它大的和比他小的,这样我每次找次大值的时候只用在更大的一部分去找就好了。
我要是将数划成k部分,这样插入的复杂度为$O(k)$,那么每次查找次大值的时候就是$O(\frac nk)$的复杂度,为了复杂度均摊,我们一般取$k=\sqrt n$,这样,总复杂度就从$O(n^2)$降为$O(n\sqrt n)$。
这个思想,叫做分块。
但显然这不是我们今天的主题。
我们想这样一个操作:
我们如果维护每个数对于的它的次大值,这样当它作为最大值的时候,不用找就能知道它的次大值,方便了删除操作。
但是显然,这将拉出一条长长的链表,我们插入操作变得很麻烦。
但这是在每个数只有一个独生子的情况下。
那我们现在开放二胎,这次不严格要求儿子是所有数中最次大的,只要不超过自己就好。
我们对于每个数,维护其长子和次子,当父亲因公离任或殉职,就挑出长子继任。
当皇帝驾崩(整个集合中的最大值被删除)时,就直接挑出其太子继位,而太子就挑他的长子来取代自己原来的位置。
依次类推,直到重孙子没有儿子。
这就完成了一次删除操作。
那这次删除操作的复杂度是多少呢?
没儿子的N重孙子是皇帝的第几世孙,我们就要继任多少次,也就是复杂度。
我们皇帝有2个儿子
这两个儿子也有各有两个儿子,皇帝有4个孙子
1,2,4,8,16,……依次类推。
当我们数到第二世时,有$1+2=3=2^2-1$个皇族亲世;
第三世,则是$1+2+4=7=2^3-1$个。
也就是说,当有$n$世皇室的时候,我们就可以处理$2^n$级别的皇亲皇子了。
换句话说,当我们有$n$个儿子在族谱里时,最多只有$log_2n$世皇室。
这样,我们皇帝驾崩一次的复杂度就是$O(logn)$的复杂度了。
那,有外戚要入族(插入元素)呢?
既然是外戚,就得一层层升级来看自己的辈分。
先找一个辈分最高但儿子不足两个的皇室认个义父,找辈分高的能让辈分最低的皇室辈分尽量高,这样保证要连续继位的复杂度不会退化。
然后,毕竟是外戚要来专权,本身还是有些实力的,所以当实力足够(自己的值比其父亲大)时,会把其父亲踹下来,自己提高一个辈分,而本来的父亲当自己的儿子。显然,另一个儿子依然可以乖乖当自己儿子。
就这样一层层的打怪父升级,要是实力不够就不打了乖乖当儿子,否则直到把皇帝弄成太子才会停下来。
这样,易证,外戚实力入侵一次也最多把自己的所有长辈全部干掉就好了,复杂度$O(logn)$。
于是,插入和删除操作都变成了$O(logn)$的复杂度了。
用算法语言表达上述内容即是:
删除操作:
将树的根节点值清空,选择其左右儿子节点中值更大的节点赋值给它,在递归那个儿子,直到递归到叶子节点结束。
插入操作:
在这颗完全二叉树的末尾位置加一个元素,然后递归向上更新,不断与父节点比较,如果比父节点更大就交换两值。2024-07-08T12:22:20.png 2024-07-08T12:31:26.png
这,就是二叉堆了。
当然,这个堆既然叫堆,有些名字就要改,比如树根就叫堆顶,删除树根元素就叫弹出堆顶元素,等等。
写成代码一点不难,比快排的细节少多了。
由于是一个二叉树,我们就用写线段树用的技巧:广搜顺序编号,根节点编号为1,找规律不难发现一个数的左儿子的自己编号的两倍,右儿子编号是左儿子编号加一。
这样我们就能迅速的写出一个堆了。
#include<iostream>
#include<cstdlib>
#include<vector>
using namespace std;
template <typename Val>
class heap{
public:
heap(){size_h=0;}//初始化堆的大小为0
void push(Val x){size_h++;while(v.size()<=size_h)v.push_back(0);v[size_h]=x;up(size_h);}//插入元素
void pop(){_swap(v[1],v[size_h]),size_h--,down(1);}//弹出堆顶
Val top(){return v[1];}//返回堆顶元素值
int size(){return size_h;}//返回堆的大小
void print(){for(auto i:v)cout<<i<<' ';cout<<endl;}//调试用的
private:
vector<Val>v;int size_h;//堆的本体
void _swap(Val &a,Val &b){Val tmp=a;a=b;b=tmp;}//定义一个swap(其实不用)
void up(int x){if(x==1)return;if(v[x>>1]<v[x])_swap(v[x>>1],v[x]),up(x>>1);}//向上更新
void down(int x){//向下更新
int son=x<<1;
if(son>size_h)return;else if(son<size_h && v[son]<v[son+1])son++;
if(v[x]<v[son])_swap(v[son],v[x]),down(son);
return;
}
};
struct node{
int val;
node(){}
node(int x){val=x;}
bool operator<(const node &x)const{
return val>x.val;//重载运算符使得大根堆变小根堆
}
};
heap<node>a;
int n;
int main(){
srand(time(0));
cin>>n;
int t;
for(int i=0;i<n;i++)cin>>t,a.push(node(t));//压入堆
for(int i=0;i<n;i++,a.pop())cout<<a.top().val<<' ';//弹出堆
return 0;
}