信竞教学——队列、栈、递归

信息竞赛教学——队列、栈、递归及其变化

队列:基本定义、优先队列、单调队列
栈:基本定义、单调栈
搜索:递归、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 xw,则将 x x x 从队首出队。

由于每个元素只入队/出队一次,故遍历 n n n 个元素时复杂度为 O ( n ) \Omicron (n) O(n)
该算法应用广泛,在日后还可以用于优化动态规划。在实现时,需要注意诸多细节,所以还请多加练习。

下面是一道经典例题:
luogu P1886 滑动窗口 /【模板】单调队列

栈与单调栈

栈(Stack)与队列相对,添加与删除均只针对栈顶操作,具有“后进先出”的特点。数组模拟请自己实现。

单调栈与单调队列类似,均为保持最大(小)值,不过单调栈的最大(小)值位于栈顶,故 出栈与入栈的操作与正常栈完全相同。它的一个常见功能是寻找单侧第一个大于(或小于)自身的元素

需要用到单调栈的情况往往特征较为鲜明,故不作过多讨论。
下面给出一道例题:
luogu P5788 【模板】单调栈


需要指出的是,非递归实现dfs(深度优先搜索)依赖于栈,bfs(广度优先搜索)的唯一实现方式依赖于队列。在不远的将来,我们会看到它们的再次出现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值