当然也可以不看 ==> 阅读我的文章前请务必先阅读此文章! 都是废话
这个文章里有视频,不知道为什么一点开文章就会播放,可以先翻到最后暂停一下再回来看
目录
阅读文章须知
为了得到良好的阅读体验,请具备稍微完备的C++语法知识
在文章中出现类似[1]的标志,作者会在文章最后进行解释说明
注:只看蓝色加粗字加速阅读,红色加粗为误区或易错
在标题前打*号表示可以选读
引言
优先队列和单调队列是一个说难不难,说简单也不简单的东西,一般混迹于图论与动态规划之间,关键时刻可以救人一命,所以本篇文章较为详细的讲解一下优先队列和单调队列,结果还是发现一篇文章讲不完,这篇文章先讲优先队列
为何先讲优先队列?并不是因为优先队列实现起来更加简单,相反,它的实现比单调队列难很多,但是——C++有优先队列的STL容器!
优先队列的概念
优先队列会保证它的头元素(也就是q.top())始终是优先队列里最先序[1]的那一个
比如我们创建一个元素类型为int的优先队列,那么不管我们往里面塞进多少个数,在任何时刻它的头元素都是当前赛进去所有元素的最大值(当然你也可以弹出头元素,那下一个头元素就是第二大的元素)
下图演示了元素类型为int的优先队列
优先队列的创建
就如同C++其他任何的STL容器一样
priority_queue<int> q;
表示创建了一个名字为q的优先队列,优先队列中的元素类型是int,元素类型是可以多种多样的,比如下面这个
struct node
{
int x, y, z;
};
priority_queue<node> a;
priority_queue<pair<int, string>> b;
其中优先队列a的元素类型是一个结构体node,优先队列b的元素类型是一个pair<int, string>[2]
优先队列在创建时默认是空的
优先队列的操作
假设我们创建了一个优先队列,名字为q
- q.push(v) 将元素v放入优先队列
- q.top() 返回目前优先队列中的最先序元素 易错:是返回而不会弹出!记得pop掉!
- q.pop() 将目前优先队列中的最先序元素弹出
*还有一些不常用的:
- q.empty() 返回一个bool类型,true表示空,false表示非空
- q.size() 返回优先队列的元素个数
优先队列的技巧
如果类型是结构体
如果你定义了一个结构体,并将该结构体作为 优先队列的元素,如下
struct node
{
//something
};
priority_queue<node> q;
那么你必须重载一个小于号,定义你所谓的 “一个node小于另一个node”
bool operator< (const node &a, const node &b) //在结构体外定义
{
//do something
return 一个bool类型; //如果a<b,就返回1,否则为0
}
举个例子,比如我的node有id,val两个成员,我说,val越小,那么这个node就越小,代码如下
struct node
{
int id, val;
};
bool operator< (const node &a, const node &b)
{
return a.val < b.val;
}
priority_queue<node> q;
这样,优先队列的头元素就是最大的node啦
实现头元素是最小的
如果类型是int
方法一
一个很妙的算法是:将每一个元素变成它的相反数加入优先队列,这样,最小的就变成最大的而在头元素中,取下来时再取相反数即可
*方法二
其实STL的priority_queue容器并不只有一个参数,它还有两个默认参数
priority_queue<int, vector<int>, less<int> > q;
第二个参数我们不讲,只需要知道他是一个容器而已就行,重点是第三个参数:less在英文中是“更少的”的意思,在此处表示从大到小,因此最大的在最前面(头元素),所以,只需要将less改为greater“更大的”就行啦,
priority_queue<int, vector<int>, greater<int> >
易错1:两个“>”不要靠在一起,最好中间空个格,可能会误认为位移符号
易错2:如果你要修改第三个参数,那么就必须将第二个参数也加上,比如以下代码就不合法
priority_queue<int, greater<int> > q;
注:如果你不需要后面两个参数,完全可以不写,比如文章的其他代码就没写,编译放心能过~
如果类型是结构体
方法一
还记得上面的重载小于号吗?只要将返回的bool类型反过来就可以了
原来最小的就变成了最大的放在了最上面了
*方法二
如果你觉得方法一将大的说成小的,小的说成大的这种方法非常晕,接下来这个方法就适合你
与int类似,使用greater<node>
priority_queue<node, vector<node>, greater<node> >
易错:在greater<node>的内部使用的是大于号,所以你需要重载一个大于号而不是小于号!
误区:不能以为第三个参数是一个函数(实际上是一个类型)——不能定义一个返回类型为bool的cmp函数作为第三个参数!
*如果一定要使用函数来比较结构体大小
sort函数习惯了怎么办
一定要定义一个cmp函数来比较结构体大小
那么用函数decltype()包裹cmp函数就可以作为第三个参数
bool cmp(node a, node b)
{
return ...;
}
priority_queue<node, vector<node>, decltype(cmp) > q;
优先队列的应用(重要)
问题
假如一个学校有10个班级,每一个班级都有一个本班按成绩从高到低的列表,那么我要寻找整个学校的前20名,怎么办?
解
如果你没学过优先队列,那么你肯定会想到一个非常朴素的算法:所有人放进一个数组,再排序,然后再取出数组前20个,时间复杂度明显O( nlogn )
但是,如果你知道优先队列,那么你可能会想到一个极其优美的算法
首先,我们将所有班级的第一名放入优先队列,那么此时的头元素就一定是全校第一(显然,如果你考了 全班第一 ,那么你还有可能是 全校第一 ,但你如果考了 全班第二 ,那你不可能是 全校第一),现在,我们把头元素输出,并将它弹出优先队列,接下来该竞选全校第二了,然后进行一个非常关键的操作:
将这个 年级第一 所在班级 的第二名 加入优先队列
为什么?
因为当你是 班级第二名 时,只有在你的 班级第一 也是 全校第一 的情况下,你才有可能是 全校第二。(因为如果你的 班级第一 不是 全校第一 ,那么你的 班级第一 最多算 全校第二 ,那你最多也只能算 全校第三 )也就是说,这个班级的 班级第二 是有竞选 全校第二 的资格的
这时,优先队列的头元素一定是全校第二
是不是发现了端倪呢?我们继续
此时我们输出头元素,并将它弹出优先队列,接下来就是那个关键的操作:将全校第二所在班级的下一名加入到优先队列
如此循环往复,每一次取出来的就一定是当前的第一名
如果你看的云里雾里,可以看以下的感性理解
感性理解
(做OI题我最喜欢的就是感性理解,很多算法原理感性理解一下就行了,严谨的数学证明我不怎么会)
首先将所有的全班第一放入优先队列,那么此时优先队列的头元素一定是全校第一,比如他是一班的小明,而一班的第二名叫小李。有一天,小明严重违纪被退学了(头元素弹出),那么此时一班的班级第一就是小李(头元素所在班级的下一个元素进入优先队列),而此时的全校第一就应该是在其他班级第一和小李之中产生,假如这个全校第一又被退学了(勿喷),那么他所在班级的下一名就上来占据了班级第一,加入了争夺本次全校第一的队伍当中......如此循环往复,每一次取出的都一定是当前的全校第一名!
怎么样,是不是恍然大悟?
**优先队列的原理及手写代码
本段高度选读
如果你细心阅读这篇文章,你可能会奇怪,为什么我每一次将元素放入的是“优先队列”而不直接简写为“队列”?为什么要把队首故意说成“头元素”?
其实,优先队列并不是一个真正的队列!在它的内部实现的是一个堆!
写到这里非常的劳累,有点想偷懒,所以放一个视频(其实动画也更容易理解)
<iframe allowfullscreen="true" data-mediaembed="bilibili" frameborder="0" id="d7lZdZV5-1721912087391" src="https://player.bilibili.com/player.html?aid=297973330"></iframe>【从堆的定义到优先队列、堆排序】 10分钟看懂必考的数据结构——堆
接下来就是手写代码实现小根堆(没有重载小于号,但不是关键)
注:视频中的堆存储下标是从0开始的,我习惯从下标1开始
这样root的子树节点就是root*2和root*2+1,而父亲节点直接root/2就行了
对了我不喜欢上滤下滤的叫法,我觉得上浮下沉跟好听一些
struct pri_que //这里实现的是小根堆
{
node a[N + 5]; //储存堆
int tail = 0; //堆的最后一位
void push_up(int id) //上浮
{
if(id == 1) return ; //如果是根节点就返回
int fa = id >> 1; //计算父亲节点的下标
if(a[id] < a[fa]) //如果父亲节点大于子节点,也就是不合法(小根堆)
{
swap(a[id], a[fa]); //交换父子俩
push_up(fa); //有可能导致上一对父子再次不合法,继续上浮
}
return ;
}
void push_down(int id) //下沉
{
int l = id << 1, r = l + 1; //计算左儿子和右儿子
int t; //保存儿子中最小的那个
if(l > tail) return ; //如果左儿子都比最后一个节点大,那肯定是叶节点,返回
if(r > tail) t = l; //如果左节点存在,但右节点不存在,那么t直接等于左节点下标
else t = a[l] < a[r] ? l : r; //如果左右节点都存在,t存最小的那个
if(a[id] > a[t]) //如果父节点小于最小的子节点
{
swap(a[t], a[id]); //交换父子
push_down(t); //继续下沉
}
return ;
}
void push(node x) //加入优先队列
{
tail++;
a[tail] = x; //将元素插入最后一位
push_up(tail); //上浮
}
node top() //最小的元素
{
return a[1]; //小根堆直接返回堆顶呗~
}
void pop() //弹出
{
swap(a[1], a[tail]); //交换最后一位和堆顶
tail--; //最后一位去掉
push_down(1); //下沉
}
};
注释
[1]最先序:如果优先队列的元素是int类型,那么元素越大,就叫做越先序,顾名思义最先序就是最大的数,但为什么不讲最大而将“最先序”呢?因为优先队列的元素有可能是一个结构体或一个类,此时两个结构体之间就没有所谓“大”和“小”的关系了,这时候就需要手动定义所谓“一个结构体先序于另一个结构体”是什么意思,具体看本文章“优先队列的技巧”处
[2]pair类型:顾名思义,pair表示“一对”元素,是C++中的一个不怎么常用的类型,下面两个代码几乎等价
pair<T1, T2> a;
struct node
{
T1 first;
T2 second;
}a;
引用a的第一个元素就是a.first,第二个元素就是a.second,不同的是,他们的类型名不同,pair的构建可以使用make_pair(T1,T2)函数,比如我要将f赋值给a的first,将s赋值给a的second(其中f是T1类型,s是T2类型)我就可以写
a = make_pair(f, s);
写在最后
上篇文章只有一个人投票(好吧就是我),下篇文章讲的的就是单调队列,然后再讲stack,遥远的路啊
标签:node,易错,队列,元素,优先,C++,int,手写,全校 From: https://blog.csdn.net/latiao520/article/details/140652789