信息竞赛教学——队列、栈、递归及其变化
队列:基本定义、优先队列、单调队列
栈:基本定义、单调栈
搜索:递归、dfs(深度优先搜索)与bfs(广度优先搜索)
Part 1: 队列
1.1 基本概念与模拟
队列(queue),是一种“先进先出”的数据结构。这种数据结构与其说是增加功能,不如说是限制功能,通过限制结构的变化方式达到指定目的。
在C++的STL库中,queue可以直接调用,但是为了方便,往往采用数组进行模拟。
演示代码如下,欢迎自己动手检验自己的知识:
#include<bits/stdc++.h>
using namespace std;
//队列的代码
namespace Queue{
const int N=10010;//队列的大小
int Q[N],Head=1,Tail=0;
//Head:头指针
//Tail:尾指针
}
using namespace Queue;
/*
如果数据结构比较简单,或者只使用了一个数据结构
则可以直接用using namespace 解除命名空间
*/
int main()
{
Tail++, Q[Tail]=2;//插入元素2
Tail++, Q[Tail]=4;//插入元素4
Q[Head]=0, Head++;//删除队首元素,此时删除的是元素2
printf("%d\n",Tail-Head+1);
//队列中元素个数可以表示为Tail-Head+1,此时为1
for(int i=Head;i<=Tail;++i)
{
printf("%d ",Q[i]);
}printf("\n");//输出整个队列
}
1.2 双端队列
双端队列,顾名思义,即两端均能进出的数据类型。使用范围不大,建议直接调用STL库中deque函数
#include<bits/stdc++.h>
using namespace std;
deque<int>q;//双端队列
//deque<Type> Name
//Type即双端队列中的数据类型
int main()
{
//若想查看任意时刻队列中元素,请复制以下代码并粘贴到相应位置
/*
printf("双端队列中现有元素为:");
for(int i=0;i<q.size();++i){
printf("%d ",q[i]);
}printf("\n");
*/
cout<<q.empty();
q.push_back(2);
q.push_front(3);
q.push_back(4);
q.push_back(1);
printf("%d\n",q.front());//队首元素
printf("%d\n",q.back());//队尾元素
printf("%d\n",q.size());//元素个数
printf("双端队列中现有元素为:");
deque<int>::iterator it;
for(it=q.begin();it!=q.end();it++)
{
printf("%d ",*it);
}printf("\n");
//另一种遍历方式
q.insert(q.begin()+1,5);
//双端队列也支持插入操作,但不常用(与基本功能不符)
q.pop_back();
printf("双端队列中现有元素为:");
for(int i=0;i<q.size();++i){
printf("%d ",q[i]);
}printf("\n");
cout<<q.empty()<<endl;
q.clear();
cout<<q.empty();
}
1.3 优先队列(堆)
优先队列(priority queue)一般通过 堆 来实现,其功能为动态维护最大(小)值,修改(插入、删除)的复杂度为
O
(
log
n
)
\Omicron (\log n)
O(logn)。
堆的结构为一棵二叉树,若每个节点的权值均不大于其父节点,则称其为大根堆,否则称其为小根堆。可以看出,大(小)根堆中最大(小)权值一定出现在堆顶。
在此引入维护的概念,即 在某一次修改后,为保持数据原有结构而进行的操作 。以堆为例,当一个元素x被插入大根堆中,也即最右侧的叶子节点处,我们进行如下操作:
- 将其与父节点 f f f比较,若 v a l [ f ] < v a l [ x ] val[f]<val[x] val[f]<val[x],则交换两者权值,并将f于f的父节点再次比较
- 若权值已经来到堆顶,或出现 v a l [ f ] ≥ v a l [ x ] val[f] \geq val[x] val[f]≥val[x],则停止操作
可以发现,单次上述操作的次数不会超过二叉树层数,也即 log n \log n logn次,所以单次操作复杂度为 O ( log n ) \Omicron (\log n) O(logn)。
对于删除操作,则先将根节点 R o o t Root Root与一叶子节点 x x x交换,随后删去 R o o t Root Root,对位于根节点处的 x x x 执行上述过程的逆过程:取 x x x 权值最大的子节点 s s s,若 v a l [ x ] < v a l [ s ] val[x]<val[s] val[x]<val[s],则交换 x x x 与 s s s ,直至迭代至二叉树底部或 v a l [ x ] ≥ v a l [ s ] val[x] \geq val[s] val[x]≥val[s]
具体实现上,可以直接调用STL库中的 priority_queue,相关用法如下:
#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> pii;
//对于某一数据类型名称的定义,最好采取typedef
//声明堆:priority_queue<TypeName> q;
priority_queue<int,vector<int>,less<int> > q1;//大根堆,元素类型为int
priority_queue<pii,vector<pii>,greater<pii> > q2;//小根堆,元素类型为pair<int,int>
int main()
{
q1.push(1);
q1.push(2);
q2.push(make_pair(1,233));
q2.push(make_pair(2,1e9+7));
cout<<q1.top()<<endl;
cout<<q2.top()<<endl;
//q.empty()
//q.size()
//q.pop()
}
1.4 单调队列(重点)
单调队列,顾名思义,即为元素单调的队列,满足元素递增(递减)和只对队首和队尾操作两条要求,但这里的“队列”与传统的队列有一定区别。它被用于维护某一范围内元素的最大(小)值,一般针对线性结构(如数组),范围一般为移动定长区间。
具体地,单调队列(以递减队列为例)包含两种操作:
- 入队:对于进入规定范围
w
w
w 的元素
x
x
x ,将所有小于
x
x
x 的元素删除(也即从队尾出队),随后将
x
x
x 推入队列
补充:由于单调性,小于 x x x 的元素均位于队尾,故删除操作等价于从队尾出队 - 出队:除上述出队方式外,若某一时刻 x ∉ w x \not\in w x∈w,则将 x x x 从队首出队。
由于每个元素只入队/出队一次,故遍历
n
n
n 个元素时复杂度为
O
(
n
)
\Omicron (n)
O(n)。
该算法应用广泛,在日后还可以用于优化动态规划。在实现时,需要注意诸多细节,所以还请多加练习。
下面是一道经典例题:
luogu P1886 滑动窗口 /【模板】单调队列
栈与单调栈
栈(Stack)与队列相对,添加与删除均只针对栈顶操作,具有“后进先出”的特点。数组模拟请自己实现。
单调栈与单调队列类似,均为保持最大(小)值,不过单调栈的最大(小)值位于栈顶,故 出栈与入栈的操作与正常栈完全相同。它的一个常见功能是寻找单侧第一个大于(或小于)自身的元素。
需要用到单调栈的情况往往特征较为鲜明,故不作过多讨论。
下面给出一道例题:
luogu P5788 【模板】单调栈
需要指出的是,非递归实现dfs(深度优先搜索)依赖于栈,bfs(广度优先搜索)的唯一实现方式依赖于队列。在不远的将来,我们会看到它们的再次出现。