栈和队列以及应用讲义P1
1 队列
队列是一种特殊的线性数据结构,只能再前端(队头)进行删除操作,后端(队尾)进行插入操作,遵循先入先出的原则。
一般情况下,我们利用一个数组q[SIZE]以及h和t两个变量来描述一个队列,数组q[]中对应元素q[h…t] 为队列中的元素。
当有元素从队尾入队时 我们将描述队尾元素下标的变量t加1。
并且将q[t] = x(x时那个新插入的元素)。当有元素从队头出队时,将变量h加1。此时原来的队头元素就会从我们所描述的队列中移除。
我们也可以使用c++库中自带的队列 queue。
queue<int>q;
q.push(x);//将x从队尾入队
q.pop();//删除队头元素
q.front();//返回队头元素的值
q.empty();//队列为空返回1,否则返回0
q.size();//返回队列的元素个数
q.back();//返回队尾元素
假如我们定义了一个空的队列 queue<int>q;
依次执行下列语句,队列内部元素分别对应:
q.push(1); 队头<- 1->队尾
q.push(2); 队头<- 1,2 ->队尾
q.push(3); 队头<- 1,2,3->队尾
q.pop(); 队头<- 2,3->队尾
q.push(0); 队头<- 2,3,0 ->队尾
q.push(4); 队头 <-2,3,0,4->队尾
q.pop(); 队头<-3,0,4->队尾
我们可以结合上述实例来理解队列的具体实现。
2 广度优先搜索
给出上面这一张图,如何找出从A点出发,到达每一个点的最小步数呢(假设每过一条边,步数+1)?
这里我们显然要遍历这整张图,但是题目要求最小步数,那么我们就显然是要按照一定的顺序去求解的,这就是所谓的广度优先的顺序去遍历。
广度优先搜索的算法思想是维护一个队列,用于存放节点(状态)信息。当访问到一个节点后,将其出队,并且把与之相连的点(没有被访问过的)全部加入队列,并更新最小步数(也就是本题所求的答案)。
根据我们1 中所给出的队列先入先出的性质,加之本题所有边的权都是1,所以队列可以很好的满足让先入队的点优先作为出发点,往后遍历其他点,来保证所有点的步数被更新时,都是最小步数。右图就是左图的广度优先搜索遍历的顺序,大家可以根据上图手动模拟一遍来加深理解。
手动模拟过程··································································································
首先开一个空的队列
A作为起点,被加入队列。
然后开始bfs,循环的推出条件是队列为空。
首先,队头是A,A被移出队列,与A相连的B,E,D被加入队列。
然后,队头为B,B被移出队列,与B相连并且不被访问过的点仅有C,C被加入队列。
再然后,队头为E,E被移出队列。
. . . . . . . . . . .
最后直到所有点都不再被加入队列,并且队列为空,算法就结束了。
··························································································································
除此之外,广度优先搜索被更多地应用在状态求解的问题上,而不是遍历图
看这样一个问题。
例题:有这样一个奇怪的电梯,每一层只有“上”和“下”两个按钮,并且每一层都有一个对应的值Ki,当按下按钮时,只能向上或者向下移动Ki层,问:从楼层A到楼层B,需要按几次按钮?
显然,这题也是一个求最小步骤的问题,但是和上面这个问题不一样的是,这题没有明确的点和边,那么如何搜索到所有的答案呢?
其实上一题中的点可以看成是这一题中的状态,而边就是当前状态可以到达的其他状态,并且我们还要记录被访问过的楼层,以免搜索的重复。一般我们把状态定义成一个结构体,包含当前状态所在的楼层编号和步骤数。
和上一题一样,首先把起始楼层加入队列,然后循环退出条件依然是队列为空。
然后每次循环把队头移出队列,将队头状态可以到达的楼层加入队列就可以了。
注意,搜索问题一般还需要判断当前状态可以到达的状态是否合法,非法的状态是不能够加入队列的。
此外,还有像八数码这类的启发式搜索,或者使用哈希来避免搜索重复的问题。
3 拓扑排序
本节内容开始之前,先引入一些图论的相关知识··························································
有向图:所有边都具有方向,也就是说有一条边A->B那么只能从A到B,不能从B到A。
点的入度:有多少边与当前点相连,并且指向当前节点,就是这个点的入度。
点的出度:有多少边与当前点相连,并且不指向当前节点,就是这个点的出度。
点的出边:与该点相连,并且不指向该点的边,称为这个点的出边
点的入边:与该点相连,指向该点的边,称为这个点的入边。
如何使用STLvector存一张图?
首先定义一个vectorvector<int>G[MAXN]
当点v与u之前存在一条由v指向u的边时 G[v].push_back(u)
当我们要遍历所有点u能够到达的点时(也就是说,所有与u有边相连,并且边是指向其他的点而不是点u)
for( auto v:G[u] )
{
cout << v << endl;
}
为了方便,这里不适用迭代器访问
··························································································································
拓扑排序指的是,有些点,要访问这个点,必须先访问它的前置点,(前置点:与该点有边相连,并且有向边的方向指向当前节点的点),所有的合法访问方法。
例题
显然,为了尽量多的游览城市,当前城市的最大游览城市数量,一定是从与当前城市有道路直接相连,并且再当前城市的西边的城市中转移过来。
为了方便描述题目,我们将所有城市看成有向图中的点,所有城市之前的道路看成边,指向东边的城市。
那我们遍历的起始城市是那些没有城市位于当前城市西边的城市。也就是说入度为0的点。
将那些点加入队列中。
拓扑排序的过程有点像广度优先搜索,但是不需要防止重复遍历的情况发生。
退出条件是队列为空。maxp[MAXN]数组用于记录以当前节点为终点,初始值为1,能合法访问的最大点数(也就是以当前城市为终点,最多能游览多少城市)
每次从队头取出一个点
u
u
u并且移出队列,访问与该节点相连(出边)的所有节点
v
v
v,访问时更新所有被访问到的点的maxp,maxp[
v
v
v]=max(maxp[
u
u
u]+1,maxp[
v
v
v])(这里可以思考一下为什么是这样),并且将所有
v
v
v的入度减去1(相当于
u
u
u从图中被移除了),如果有出现新的入度为0的点,就将其入队。直到没有点被入队,并且队列为空,算法结束。
这样我们就得到了所有点的maxp也就是答案。
4 栈
堆栈(stack)是一种只能在顶端插入删除和访问操作的线性数据结构,遵循先进后出,后进先出的原则。
在堆栈顶端插入元素
在堆栈顶端删除元素。
在算法竞赛中,不考虑内存管理的问题,我们用数据模拟栈;
int stk[MAXN]; 用于存储栈中元素
int top; 用于存储栈顶位置
intz(stk);初始化一个栈,top=0即可
stk[++top]=x;插入元素x
top --;删除顶端元素
同时 在c++的自带函数中,有stack<ELMtype>Name,可以方便的进行栈的操作:
stack<int>s; 定义一个类型为int的空栈 | 栈内部 |
s.push(1); 在顶顶端插入一个元素 1 底<1>顶
s.push(2); 在顶端插入一个元素 2 底<1,2>顶
s.pop(); 删除一个元素 底<1, >顶
s.push(3) 底<1,3>顶
s.empty(); 查看栈内部是否为空,为空返回1
s.size(); 返回栈内部元素个数
s.top(); 返回栈顶元素 3
执行递归的过程,就像堆栈一样,把上一层递归的内容先存到一个堆栈中。
我们把dfs的入栈序列称为dfs序(dfN)
5 深度优先搜索
回顾一下这张图,图中的数字是这张图从A点出发的广度优先搜索顺序,那与此对应,我们还有一种搜索方法称为深度优先搜索,与广度优先搜索先把最近的点优先遍历的思路不同,深度优先搜索的思路是在当前状态(节点)能继续往下遍历,那就先从当前状态(节点)选择一个能直接到达的状态(节点)往下遍历,直到无法继续往下遍历,再回溯到上一个节点,继续执行相同的操作,是一种基于递归的算法。
那么上图的深度优先搜索的顺序是什么?
A -> D -> F -> H -> I -> E -> B ->C ->G
在深度优先遍历到I时,发现无法继续向下遍历,这是,我们就执行回溯操作,直到回溯到F,发现能继续往下遍历,遍历完全部节点。
在解决一些问题时,通过递归来枚举状态,也可以称为深度优先搜索,因为其本质也是通过递归来不断往下转移,直到无法向下一个状态转移,然后回溯。
比如:输出1 ~ 5数字中所有全排列
n = 5;
void dfs(int step) {
if (step == n + 1) { // 边界
for (int i = 1; i <= n; i++) {
cout<< a[i];
}
cout << endl;
return;
}
for (int i = 1; i <= n; i++) {
if (vis[i] == 0) { //判断数字i是否在正在进行的全排列中
vis[i] = 1;
a[step] = i;
dfs(step + 1);
vis[i] = 0; //这一步不使用该数 置0后允许下一步使用
}
}
return;
}
基本思路也是枚举当前位的下一位能是哪些数字,直到最后一位,然后回溯。
6 基于记忆化的深度优先搜索
在使用深度优先搜索解决一些问题的时候,经常会遇到需要搜索重复状态,造成时间上的冗余。这里我们就可以使用基于化的方式,来降低时间复杂度。
记忆化的意思就是将所有状态的答案在深搜中记录下来,以便于下次访问的时候直接返回答案,不用重复搜索。
这里,我们使用杨辉三角来举例:
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1
.。。。。。
令上图杨辉三角的行号位 i ,列号 j。
那么元素 f(i,j)=f(i-1,j)+f(i-1,j-1)
对于询问每一个状态的值,我们可以通过深度优先搜索来递归完成,也就是递归 dfs(i,j)=dfs(i-1,j)+dfs(i-1,j-1)。
在这个过程中,我们发现,有一大部分状态被重复搜索了,造成了时间上的浪费,所以对于每一次,搜到f(i,j)状态时,我们都用数组将 f(i,j) 所对应的答案记录下来,a[i][j]=dfs(i,j)。如果下次访问到这个状态,直接返回数组中对应的值。
上述求杨辉三角的方法,其实就是记忆化dfs预处理组合数的过程:
(图片来源网络)
End
如果大家对以上算法还有疑惑,可以看看oiwiki上的讲解,也可以通过写题来加深理解。
浙江科技学院ACM集训队2021/11/28