浙江科技学院ACM新生培训栈和队列以及应用讲义

本文介绍了数据结构中的栈和队列,阐述了它们的基本特性和操作,如C++标准库中的实现。接着讨论了广度优先搜索(BFS)和深度优先搜索(DFS)的应用,包括图的遍历和最短路径问题。拓扑排序的概念也被提及,以及如何在有向图中进行操作。最后,文章提到了递归与堆栈的关系,并通过示例展示了深度优先搜索在解决全排列问题时的运用。
摘要由CSDN通过智能技术生成

栈和队列以及应用讲义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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值