二叉堆初探

一、二叉堆基础

1. 二叉堆是什么?

二叉堆就是一棵完全二叉树。

什么是完全二叉树?定义d为树的深度,则一棵完全二叉树的前(d-1)层是满的,而第d层的节点从右往左有若干连续缺失。

并且二叉堆有优先级,有的二叉堆每一个父亲都比他的儿子小,称为小根堆;反之称为大根堆。

这样说比较抽象,放一张小根堆的图:

2. 如何存储一个二叉堆?

主要有两种存储方法。

第一种是链表存储,记录左儿子右儿子,父亲与值;

int root=1;//堆顶
int siz;//堆的大小
struct Tree
{
    int ls,rs,fa,val;
}tree[100010];

第二种是用数组存储,开两倍空间,乘2表示左儿子,乘2加1表示右儿子,除以2下取整表示父亲。这种表示方式较为方便。

int siz;//堆的大小
int tree[200020];//tree[1]即为堆顶

之后都用这种写法。堆也默认为小根堆。

3. 二叉堆的操作

注:接下来的实现代码都是实现小根堆的

1. 最基础的操作

就不用说了。时间复杂度\(O(1)\)

inline int top(){return tree[1];}
inline int size(){return siz;}
inline bool empty(){return siz==0;}
2. 堆的插入

在堆底插入,再依次与其父亲作比较,选择是否交换。时间复杂度\(O(\log n)\)

举个例子:向刚刚的小根堆插入数字2。

首先我们在堆底插入2:

2<5,交换:

2<3,交换:

2>1,不能再交换了。

inline void push(int xx)
{
    tree[++siz]=xx;
    int now=siz;
    while(now!=1)
    {
        int nxt=now>>1;
        if(tree[nxt]>tree[now])swap(tree[nxt],tree[now]);
        else break;
        now=nxt;
    }
    return;
}
3. 堆的删除

删除别的元素是没有意义的,故我们只讨论删除堆顶的情况。
先将堆顶堆底交换,删掉堆底,再将堆顶向下判断和哪一个孩子交换。时间复杂度\(O(\log n)\)

还是刚才的例子:我们要删除堆顶1。首先将堆顶堆底交换:

然后把堆底删掉:

接下来我们来进行调整,以保持堆的性质。两个儿子中2更小,故与2交换:

3更小,故与3交换:

5<9,无需再交换了。

inline void pop()
{
    swap(tree[siz--],tree[1]);
    int now=1;
    while((now<<1)<=siz)
    {
        int nxt=now<<1;
        if(nxt+1<=siz&&tree[nxt+1]<tree[nxt])nxt++;
        if(tree[nxt]<tree[now])swap(tree[now],tree[nxt]);
        else break;
        now=nxt;
    }
    return;
}
4. 模板

模板题在这里

#include <iostream>
#include <cstdio>
using namespace std; 
int n,opt,x;
//------------模板开始-------------
int siz,tree[2000020];
inline int top(){return tree[1];}
inline int size(){return siz;}
inline bool empty(){return siz==0;}
inline void push(int xx)
{
    tree[++siz]=xx;
    int now=siz;
    while(now!=1)
    {
        int nxt=now>>1;
        if(tree[nxt]>tree[now])swap(tree[nxt],tree[now]);
        else break;
        now=nxt;
    }
    return;
}
inline void pop()
{
    swap(tree[siz--],tree[1]);
    int now=1;
    while((now<<1)<=siz)
    {
        int nxt=now<<1;
        if(nxt+1<=siz&&tree[nxt+1]<tree[nxt])nxt++;
        if(tree[nxt]<tree[now])swap(tree[now],tree[nxt]);
        else break;
        now=nxt;
    }
    return;
}
//------------模板结束-------------
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&opt);
		switch(opt)
		{
			case 1:
			{
				scanf("%d",&x);
				push(x);
				break;
			}
			case 2:printf("%d\n",top());break;
			case 3:pop();break;
		}
	}
	return 0;
}

4. 简单应用

1. 合并果子

这道题
简单的堆优化贪心。

#include <iostream>
#include <cstdio>
using namespace std;
int n,x,y,cnt;
//------------模板开始-------------
int siz,tree[200020];
inline int top(){return tree[1];}
inline int size(){return siz;}
inline bool empty(){return siz==0;}
inline void push(int xx)
{
    tree[++siz]=xx;
    int now=siz;
    while(now>1)
    {
        int nxt=now>>1;
        if(tree[nxt]>tree[now])swap(tree[nxt],tree[now]);
        else break;
        now=nxt;
    }
    return;
}
inline void pop()
{
    swap(tree[siz--],tree[1]);
    int now=1;
    while((now<<1)<=siz)
    {
        int nxt=now<<1;
        if(nxt+1<=siz&&tree[nxt+1]<tree[nxt])nxt++;
        if(tree[nxt]<tree[now])swap(tree[now],tree[nxt]);
        else break;
        now=nxt;
    }
    return;
}
//------------模板结束-------------
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&x);
        push(x);
    }
    for(int i=1;i<=n-1;i++)
    {
        x=top();
        pop();
        y=top();
        pop();
        cnt+=x+y;
        push(x+y);
    }
    printf("%d\n",cnt);
    return 0;
}
2. 堆排序

就是把所有的元素都放到堆里,再一个个取出。可以试试这道题

#include <iostream>
#include <cstdio>
using namespace std; 
int n,x;
//------------模板开始-------------
int siz,tree[200020];
inline int top(){return tree[1];}
inline int size(){return siz;}
inline bool empty(){return siz==0;}
inline void push(int xx)
{
    tree[++siz]=xx;
    int now=siz;
    while(now!=1)
    {
        int nxt=now>>1;
        if(tree[nxt]>tree[now])swap(tree[nxt],tree[now]);
        else break;
        now=nxt;
    }
    return;
}
inline void pop()
{
    swap(tree[siz--],tree[1]);
    int now=1;
    while((now<<1)<=siz)
    {
        int nxt=now<<1;
        if(nxt+1<=siz&&tree[nxt+1]<tree[nxt])nxt++;
        if(tree[nxt]<tree[now])swap(tree[now],tree[nxt]);
        else break;
        now=nxt;
    }
    return;
}
//------------模板结束-------------
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&x);
		push(x);
	}
	for(int i=1;i<=n;i++)
	{
		printf("%d ",top());
		pop();
	}
	return 0;
}

上面三道题的复杂度都是\(O(n\log n)\)

二、更多应用

1. STL priority_queue

在讲题目之前,先介绍一下STL中的优先队列。

优先队列其实就是堆……但只支持插入,删除堆顶等基础操作。

我们可以这样来定义一个大根堆:

#include <queue>//头文件
priority_queue<int> q;

小根堆麻烦一些:

#include <queue>
#include <vector>
#include <functional>
priority_queue<int,vector<int>,greater<int> > q;

对于结构体重载运算符即可。

#include <queue>
struct node{int x,y;};
bool operator <(node xx,node yy)
{
      if(xx.x!=yy.x)return xx.x<yy.x;
      return xx.y<yy.y;
}
priority_queue<node> q;

这是大根堆,小根堆只需改成这样:

#include <queue>
struct node{int x,y;};
bool operator <(node xx,node yy)
{
      if(xx.x!=yy.x)return xx.x>yy.x;
      return xx.y>yy.y;
}
priority_queue<node> q;

然后是一些基础操作:

q.push(x)//插入x
q.top()//返回堆顶
q.pop()//删除堆顶
q.empty()//是否为空
q.size()//元素个数

于是合并果子那题可以这样写:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <queue>
using namespace std;
int n,x,y,ans;
priority_queue<int,vector<int>,greater<int> >q;
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&x);
		q.push(x);
	}
	for(int i=1;i<n;i++) 
	{
		x=q.top();q.pop();
		y=q.top();q.pop();
		ans+=x+y;
		q.push(x+y);
	}
	printf("%d\n",ans);
	return 0;
}

简洁了许多呢(笑

2. 比较复杂的模拟

舞蹈课 这道

不难想到,我们可以用堆来维护当前差距最小的一对舞者的编号。
当然你会发现一对舞者出列最多会导致两队舞者的组合被打破……
不过用一个danced数组来标记一下就行了(
那出列不还可能会多构造出一对舞者吗?
数据结构带师:套一个链表
然而我并不会懒得写,于是选择直接暴力跳
暴力跳的复杂度好像也不会炸。。。然而我不会证明>_<
u1s1这题奇怪的细节还是有的,并且数据不给下载,一出错直接爆0。。。
更多细节见代码

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <string>
#include <queue>
#include <vector>
using namespace std;
string s;
int n,cnt,a[200010];
bool danced[200010];
struct node{int x,y;};
bool operator<(node p1,node p2)//重载运算符,里面的符号是反的是因为小根堆
{
    if(abs(a[p1.x]-a[p1.y])!=abs(a[p2.x]-a[p2.y]))return abs(a[p1.x]-a[p1.y])>abs(a[p2.x]-a[p2.y]);
    else return p1.x>p2.x;
}
priority_queue<node> q;
vector<node> ans;
int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);cout.tie(0);
    cin >> n >> s;
    for(int i=0;i<n;i++)cin >> a[i];//因为string是从0开始于是就这样偷懒了(
    for(int i=1;i<s.length();i++)
        if(s[i-1]!=s[i])
            q.push((node){i-1,i});
    while(1)
    {
        while(!q.empty()&&(danced[q.top().x]||danced[q.top().y]))//如果这一对中有人danced那么直接扔掉
            q.pop();
        if(q.empty())break;//判空(防止死循环
        cnt++;//步数增加
        ans.push_back(q.top());//答案更新
        danced[q.top().x]=danced[q.top().y]=1;//标记
        int l=q.top().x,r=q.top().y;//暴力跳指针
        q.pop();//一定要记得这个时候把堆顶扔掉,要不然之后压进来又弹出去统计了个寂寞(
        while(l>=0&&danced[l])l--;//暴力跳左指针
        while(r<n&&danced[r])r++;//暴力跳右指针
        if(l>=0&&r<n&&!danced[l]&&!danced[r]&&s[l]!=s[r])q.push((node){l,r});//判断,决定是否插入
    }
    cout << cnt << endl;
    for(int i=0;i<cnt;i++)
        cout << ans[i].x+1 << ' ' << ans[i].y+1 << endl;
    return 0;
}

3. 对顶堆

黑匣子 这道

对顶堆其实就是一个小根堆倒着摞在一个大根堆上(
那这样子的话我们可以发现就整体来看元素从下往上是大致递增的

放图:

当然也可以倒着来理解,没有差别

对顶堆中两个堆的堆顶一般用来存储特殊数据。(对于这道题来说就是第i小)
插入的时候与堆顶作比较决定插入哪个堆里,然后再调整一次两个堆来让堆顶满足查询条件。
这样的话两个堆的大小差就会始终保持在正负1以内。
图就不放了(懒

一般来说每次的查询位置变动不大,这样我们才能保证对顶堆的时间复杂度正确。(也就是平衡操作的时间复杂度为\(O(1)\)
接下来就是详细注释的代码了……

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <queue>
#include <vector>
#include <functional>
using namespace std;
int m,n,now=1,pointer,a[200010],u[200010];
//pointer就是题目中的i……当然也可以用循环变量代替,这里它的存在就显得多余了
priority_queue<int> q1;
priority_queue<int,vector<int>,greater<int> >q2;
int main()
{
    scanf("%d%d",&m,&n);
    for(int i=1;i<=m;i++)scanf("%d",&a[i]);
    for(int i=1;i<=n;i++)scanf("%d",&u[i]);
    for(int i=1;i<=n;i++)
    {
        pointer++;
        if(!q1.empty()&&!q2.empty()&&q1.size()<pointer)//因为pointer自增了,这里要维护一次平衡
        {
            q1.push(q2.top());
            q2.pop();
        }
        if(!q1.empty()&&!q2.empty()&&q1.size()>pointer)
        {
            q2.push(q1.top());
            q1.pop();
        }
        for(;now<=u[i];now++)
        {
            if(!q1.empty()&&a[now]>=q1.top())q2.push(a[now]);//与堆顶比较来决定插入哪个堆中
            else q1.push(a[now]);
            if(q1.size()<pointer){q1.push(q2.top());q2.pop();}
            if(q1.size()>pointer){q2.push(q1.top());q1.pop();}//维护平衡
        }
        printf("%d\n",q1.top());
    }
    return 0;
}

相信您做完这题后就能轻松AC这道了qwq

u1s1对顶堆真的不难,我也不知道为什么这几道题评分这么高(

原文地址:https://www.cnblogs.com/pjykk/p/11144027.html