深度优先搜索(DFS)和广度优先搜索(BFS)都是一种对可能解答路径的枚举方式,深度优先搜索每次都从一个方向一直搜索到最终状态,然后进行回溯尝试枚举其他到达最终状态的路径。回溯的过程需要存储回溯点当前的状态,递归方法可以很好的完成这个任务,在递归方法之前设置状态信息,在递归后将状态信息设置回原值。如果设计递归方法是关键,递归方法对应着深度优先搜索的策略。本文尝试从几个例子尝试探索使用深度优先搜索方法的一般设计方法。
二维矩阵两点之间的路径遍历
一个典型的问题是迷宫问题,给出一个迷宫矩阵,0代表可以走的位置,1代表墙的位置,给出起始和出口位置的坐标,求出最小步数走出迷宫的路径,并且打印出这个路径。
解决这个问题的基本想法是枚举所有的从起始位置到出口位置的路径,并记录最短的那条路径,那么应该怎么样才可以全面不漏的列举所有的路径呢?我们从一个坐标点(x,y)出发,那么经过一步可以走的方向有上下左右四个方向,也就是(x-1,y),(x+1,y),(x,y-1),(x,y+1)。计算了下一步的位置,我们需要判断,新的位置是否在矩阵内,是否是墙,是否是出口节点,如果不是上面以上特殊节点,我们就可以把新的节点作为基地继续上面的搜索过程。
很显然,以上的搜索过程可以用递归写出来,其中定义递归转移的状态是当前的的坐标,出口为下一个位置为出口位置,对于在矩阵内的不是墙,不是出口节点的搜索方法是朝4个方向进行探索。
除了输入信息:矩阵行列数,迷宫矩阵,起始坐标,输出信息:最小步数,最小步数路径,我们还需要一些辅助容器,为了方便我们对下一个位置的计算,我们定义一个4*2的二维数组,可以使用循环的方式对下一个位置进行计算,如向上可以使用(-1,0)加上原来的(x,y)进行计算。总的来说,我们需要的信息如下,可以通过声明全局变量的方式进行声明
定义好需要的信息,我们就可以设计算法获取结果,如何进行计算呢,其基本思路是下面这样
那么如何”将初始位置作为初始点进行搜索并将最优结果存储到结果容器中”,这个是算法核心,因为求的是最优的结果,所以我们不能直接在optimalStep和optimalStack进行存储,需要在算法计算的时候为它们声明一个副本,然后在到达出口节点的时候将副本step与optimalStep比较,如果step小于optimalStep,那么应该将optimalStep变成step,并且用optimalStack记录当前的stack路径。
按照上面的思路,我们的递归方法的参数应该是(x,y,step,stack),即当前的坐标,当前已走的步数,当前的路径坐标栈。因为结果会在全局变量中更新,所以不需要返回值,按此思路,设计算法如下。
以上即为深度搜索的算法,可见深度优先搜索是依赖于递归进行的,其中状态,特殊位置处理,搜索方法是三驾马车,其中状态的设计是根本。使用栈存储变量的路径是十分有效的方法,在递归方法上的路径存储基本上都是使用栈来进行,在递归回来的时候可以使用弹栈的方式继续进行搜索。其中看在递归的前后还将当前位置设置为墙,为的是方式新的状态返回到当前状态,递归返回后,我要将其设置回来。
每个状态都是递归计算的,这意味着每个状态的计算都是可以重复的,一样的,不一样之处应该在特殊位置进行处理,通常剪枝和出口都在特殊位置状态进行处理。
如此,我们便可以轻松写出深度搜索的方法
在main函数中,我们就可以直接调用dfs(start.x,start.y,0,new Stack())后获取最优的结果。
二维矩阵连通区域计算
再来一个二维矩阵的深度优先搜索例子:给出一个矩阵,其中*代表空,@代表油田,一块油田的上下左右和对角相邻位置视为连通,连通的油田是一块区域,给出一个这样的矩阵,计算出连通区域的个数。
明确我们的目的是求出所有的连通区域的个数,基本想法是遍历所有的矩阵点,对于所有的油田点进行深度搜索,目的是将与当前油田点标记为已访问状态;而对于已访问的矩阵点或者空点不做处理。
我们的输入变量有矩阵行列,矩阵,输出变量是连通区域数,我们还需要辅助变量方向数组和标记数组,其中标记数组用来标记一个节点是否已经被访问到了。
递归方法的设计,转移状态应该是当前的油田节点的坐标(x,y),计算新的节点(nx,ny)有三个特殊状态:出界,空点,已访问的点,这三种状态应该直接跳过,进行下一个方向的尝试,处理以上三个特殊状态,那么就剩下未访问的相邻油田,应该设置新的油田节点为已访问状态,并且以(nx,ny)为新基点进行递归探寻。其核心思想如下
根据以上,我们写出java代码
二叉树的最长路径
说完了二维矩阵的深度优先遍历,下面我们举个二叉树的深度优先遍历的应用,即给出一个二叉树,打印出最长的从根到叶子节点的路径。
如果仅仅是求最长的路径是多少,我们使用层次遍历即可,也就是对应于广度优先遍历,而如果要把路径求出来,需要深度优先遍历所有的路径,记录最长的那一条。
我们的输入是一个树根,需要辅助的变量有临时的路径栈和临时路径长,输出是最长的路径和路径长,临时路径栈和临时路径长可以作为方法参数传递,所以我们只需要定义全局的输出信息。
深度优先递归的状态是当前的节点,还有作为参数传递的临时路径栈和临时路径长。特殊位置点有两个,一是如果当前节点是null,则直接返回,二是如果当前节点是叶子节点,则应该比较当前的路径长度和最长的路径长度,如果当前的路径长度更长,则应该更新最长的路径长度和路径栈。
其他情况,递归遍历左子树和右子树。其实现过程如下
隐式深度优先搜索-素数环
有的问题可以通过优先搜索的方式解决,但是状态的定义并不像上面二维数组,树啊的那么容易定义,一个典型的例子就是素数环问题,给你一个数,比如6,让你将1-6所有能组成素数环的排列全部列出,素数环排列必须以1开始。所谓素数环,指的是任意相邻的数和都为素数,最后一个数和第一个数和也为素数。
我们的输入只有一个数,但是却隐含了一个数组,解决这个问题的基本想法是遍历所有的以1开始的所有排列,但是怎么遍历,肯定不能用多个循环,我们可以定义状态为当前放入的位置index,以6为例,我们尝试从放入6个位置的数,每个数从2-6中选取,如果一个状态不符合,可以立即进行回退尝试下一个数字,如果符合,则可以尝试下一个位置,直至到达容量。
为了完成这个任务,我们需要对6个数字进行标记是否处于已选入状态,如果是,则不应该再次选入。所以我们需要以下变量的支持。
我们的状态是当前已经放置的位置index,刚开始我们会将1放入,并且标记1为已选中状态。对于一个已放入状态的index,我们需要检查比较index和index-1位置的和是否为素数,这是每一个index都需要做的事情。对于特殊位置index==n,意味着位置已经放完,应该检查index位置和1位置的值和是否为素数,如果是,则成功查找到一个,将其放入到结果容器中,然后返回。搜索的过程是查找2-n中未选中的数,尝试将其放入index+1位置,并递归调用index+1位置。在此之前应该设置选中的数为已选中状态,返回的时候将其设置为未选中状态。整个思路如下
这种隐式的深度优先递归的放入位置比较难想,这里一个数就相当于一个尝试的点,需要在递归的内部进行循环遍历,这样就变相的实现了多重循环的实现。
声明
本文首发表于我的博客,欢迎关注!已委托维权骑士为本站的文章进行维权,转载须注明文章出处,作者保留文章所有权。