搜索
目录
1、深度优先搜索
一、DFS
二、基于DFS的记忆化搜索
3、基于DFS的剪枝
1) 可行性剪枝
2) 最优性剪枝
四、基于DFS的A* (迭代加深,IDA*)
2、广度优先搜索
一、BFS
二、基于BFS的A*
三、双向广搜
3、搜索题集整理
1、深度优先搜索
一、DFS
1) 算法原理
深度优先搜索即Depth First Search,是图遍历算法的一种。用一句话归纳就是:“一直往下走,走不通回头,换条路再走,直到无路可走”。
DFS的具体算法描述为选择一个起始点v做为当前结点,执行以下操做:
a. 访问 当前结点,而且标记该结点已被访问,而后跳转到b;
b. 若是存在一个和 当前结点 相邻而且还没有被访问的结点u,则将u设为 当前结点,继续执行a;
c. 若是不存在这样的u,则进行回溯,回溯的过程就是回退 当前结点;
上述所说的当前结点须要用一个栈来维护,每次访问到的结点入栈,回溯的时候出栈(也能够用递归实现,更加方便易懂)。
如图1所示,对如下图以深度优先的方式进行遍历,假设起点是1,访问顺序为1 -> 2 -> 4,因为结点4没有未访问的相邻结点,因此这里须要回溯到2,而后发现2还有未访问的相邻结点5,因而继续访问2 -> 5 -> 6 -> 3 -> 7,这时候7回溯到3,3回溯到6,6回溯到5,5回溯到2,最后2回溯到起点1,1已经没有未访问的结点了,搜索终止,图中圆圈表明路点,红色箭头表示搜索路径,蓝色虚线表示回溯路径。
图1
2) 算法实现
深搜最简单的实现就是递归,写成伪代码以下:
1 def DFS(v):
2 visited[v] =
true
3 dosomething(v)
4
for u
in adjcent_list[v]:
5
if visited[u]
is
false:
6 DFS(u)
其中
dosomething表示访问时具体要干的事情,根据状况而定,而且DFS是容许有返回值的。
3) 基础应用
a. 求N的阶乘;
令f(N) = N!,那么有
f(N) = N * f(N-1) (其中N>0)。
因为知足递归的性质,能够认为是一个N个结点的图,结点 i (i >= 1 ) 到结点 i-1 有一条权值为i的有向边,从N开始深度优先遍历,遍历的终点是结点0,返回1(由于0! = 1)。如图2所示,N!的
递归计算当作是一个深度优先遍历的过程,而且每次回溯的时候会将遍历的结果返回给上一个结点(这只是一个思想,并不表明这是求N!的高效算法)。
b. 求斐波那契数列的第N项;
令g(
N) = g(N-1) + g(N-2), (N > 2),其中g(1) = g(2) = 1,一样能够利用图论的思想,从结点N向N-1和N-2分别引一条权值为1的有向边,每次求g(N)就是以N做为起点,对N进行深度优先遍历,而后将N-1和N-2回溯的结果相加做为N结点的值,即g(N)。这里会带来一个问题,g(n)的计算须要用到g(n-1)和g(n-2),而g(n-1)的计算须要用到g(n-2)和g(n-3),因此咱们发现g(n-2)被用到了两次,并且每一个结点都存在这个问题,这样就使得整个算法的复杂度变成指数级了,为了规避这个问题,下面会讲到基于深搜的记忆化搜索。
c. 求N个数的全排列;
全排列的种数是N!,要求按照字典序输出。这是最典型的深搜问题。咱们能够把N个数两两创建无向边(即任意两个结点之间都有边,也就是一个N个结点的彻底图),而后对每一个点做为起点,分别作一次深度优先遍历,当全部点都已经标记时输出当前的遍历路径,就是其中一个排列,这里须要注意,回溯的时候须要将原先标记的点的标记取消,不然只能输出一个排列。若是要按照字典序,则须要在遍历的时候保证每次遍历都是按照结点从小到大的方式进行遍历的。
4) 高级应用
a. 枚举:
数据范围较小的的排列、组合的穷举;
b.
容斥原理:
利用深搜计算一个公式,本质仍是作枚举;
c.
基于状态压缩的动态规划:
通常解决棋盘摆放问题,k进制表示状态,而后利用深搜进行状态转移;
d.
记忆化搜索:
某个状态已经被计算出来,就将它cache住,下次要用的时候不须要从新求,此所谓记忆化。下面会详细讲到记忆化搜索的应用范围;
e.有向图
强连通份量:
经典的Tarjan算法;
求解2-sat问题的基础;
f. 无向图割边割点和双连通份量:
经典的Tarjan算法;
g. LCA:
最近公共祖先递归求解;
h.
博弈:
利用深搜计算SG值;
i.
二分图最大匹配:
经典的匈牙利算法;
最小顶点覆盖、最大独立集、最小值支配集 向二分图的转化;
j.
欧拉回路:
经典的圈套圈算法;
k.
K短路:
依赖数据,数据不卡的话能够采用2分答案 + 深搜;也能够用广搜 + A*
l.
线段树
二分经典思想,配合深搜枚举左右子树;
m. 最大团
极大彻底子图的优化算法。
n. 最大流
EK算法求任意路径中有涉及。
o. 树形DP:
即树形动态规划,父结点的值由各个子结点计算得出。
二、基于DFS的记忆化搜索
1) 算法原理
上文中已经提到记忆化搜索,其实就是相似动态规划的思想,每次将已经计算出来的状态的值存储到数组中,下次须要的时候直接读数组中的值,避免重复计算。
来看个例子,如图5所示,图中的橙色小方块就是传说中的做者,他能够在一个N*M的棋盘上行走,可是只有两个方向,一个是向右,一个是向下(如绿色箭头所示),棋盘上有不少的金矿,走到格子上就能取走那里的金矿,每一个格子的金矿数目不一样(用蓝色数字表示金矿的数量),问做者在这样一个棋盘上最多能够拿到多少金矿。
图5
咱们用函数DFS(i, j)表示从(1, 1)到(i, j)能够取得金矿的最大值,那么状态转移方程 DFS(i, j) = v[i][j] + max{ DFS(i, j-1), DFS(i-1, j) }(到达(i, j)这个点的金矿最大值的那条路径要么是上面过来的,要么是左边过来的),知足递归性质就能够进行深度优先搜索了,因而遇到了和求斐波那契数列同样的问题,
DFS(i, j)可能会被计算两次,每一个结点都被计算两次的话复杂度就是指数级了。
因此这里咱们能够利用一个二维数组,令D[i][j] = DFS(i, j),初始化全部的D[i][j] = -1,表示还没有计算,每次搜索到(i, j)这个点时,检查D[i][j]的值,若是为-1,则进行计算,将计算结果赋值给D[i][j];不然直接返回D[i][j]的值。
记忆化搜索虽然叫搜索,实际上仍是一个动态规划问题,可以记忆化搜索的通常都能用动态规划求解,可是记忆化搜索的编码更加直观、易写。
三、基于DFS的剪枝
1) 算法原理
搜索的过程能够看做是从树根出发,遍历一棵倒置的树——搜索树的过程。而剪枝,顾名思义,就是经过某种判断,避免一些没必要要的遍历过程,形象的说,就是剪去了搜索树中的某些“枝条”,故称剪枝(原话取自1999年OI国家集训队论文《搜索方法中的剪枝优化》(齐鑫))。
如图6所示,它是一棵利用深度优先搜索遍历的搜索树,可行解(或最优解)位于黄色的叶子结点,那么根结点的最左边的子树彻底没有必要搜索(由于不可能出解)。若是咱们在搜索的过程当中可以清楚地知道哪些子树不可能出解,就不必往下搜索了,也就是将链接不可能出解的子树的那根“枝条”剪掉,图中红色的叉对应的“枝条”都是能够剪掉的。
图6
好的剪枝能够大大提高程序的运行效率,那么问题来了,如何进行剪枝?咱们先来看剪枝须要知足什么原则:
a. 正确性
剪掉的子树中若是存在可行解(或最优解),那么在其它的子树中极可能搜不到解致使搜索失败,因此剪枝的前提必须是要正确;
b. 准确性
剪枝要“准”。所谓“准”,就是要在保证在正确的前提下,尽量多得剪枝。
c. 高效性
剪枝通常是经过一个函数来判断当前搜索空间是不是一个合法空间,在每一个结点都会调用到这个函数,因此这个函数的效率很重要。
剪枝大体能够分红两类:可行性剪枝、
最优性剪枝
(
上下界剪枝
)。
2) 可行性剪枝
可行性剪枝通常是处理可行解的问题,如一个迷宫,问可否从起点到达目标点之类的。
举个最简单的例子,如图7,问做者可否在正好第11秒的时候避过各类障碍物(图中的东西一看就知道哪些是障碍物了,^_^)最终取得爱心,做者每秒能且只能移动一格,容许走重复的格子。
仔细分析能够发现,这是永远不可能的,由于做者不管怎么走,都只能在第偶数秒的时候到达爱心的位置,这是他们的曼哈顿距离(两点的XY坐标差的绝对值之和)的奇偶性决定的,因此这里咱们能够在搜索的时候作奇偶性剪枝(可行性剪枝)。
相似的求可行解的问题还有不少,如N (N <= 25) 根长度不一的木棒,问可否选取其中几根,拼出长度为K的木棒,
具体就是枚举取木棒的过程,每根木棒都有取或不取两种状态,因此总的状态数为2^25,须要进行剪枝。
用到的是剩余和不可达剪枝(随便取的名字,即当前S根木棒取了S1根后,剩下的N-S根木棒的总和 加上 以前取的S1根木棒总和若是小于K,那么必然不知足,不必继续往下搜索),这个问题实际上是个01背包,当N比较大的时候就是动态规划了。
3) 最优性剪枝
(
上下界剪枝
)
最优性
剪枝通常是处理最优解的问题。以求两个状态之间的最小步数为例,
搜索最小步数的过程:通常状况下,须要保存一个“当前最小步数”,这个最小步数就是当前解的一个下界d
。在遍历到搜索树的叶子结点时,获得了一个新解,与保存的下界做比较,若是新解的步数更小,则令它成为新的下界。搜索结束后,所保存的解就是最小步数。而当咱们已经搜索了k歩,若是可以经过某种方式估算出当前状态到目标状态的理论最少步数s时,就能够计算出起点到目标点的理论最小步数,即估价函数h = k + s,那么当前状况下存在最优解的必要条件是h < d,不然就能够剪枝了。最优性剪枝是不断优化解空间的过程。
四、基于DFS的A*(迭代加深,IDA*)
1) 算法原理
迭代加深分两步走:
一、枚举深度。
二、根据限定的深度进行DFS,而且利用估价函数进行剪枝。
2) 算法实现
迭代加深写成伪代码以下:
1 def IDA_Star(STATE startState):
2 maxDepth = 0
3
while
true:
4
if( DFS(startState, 0, maxDepth) ):
5
return
6 maxDepth = maxDepth + 1
图8
3) 基础应用
如图8所示,一个“井”字形的玩具,上面有三种数字一、二、3,给出8种操做方式,A表示将第一个竖着的列循环上移一格,而且A和F是一个逆操做,B、C、D...的操做方式依此类推,初始状态给定,目标状态是中间8个数字相同。问最少的操做方式,而且要求给出操做的序列,步数同样的时候选择字典序最小的输出。图中的操做序列为AC。
大体分析一下,一共24个格子,每一个格子三种状况,因此最坏状况状态总数为3^24,但实际上,咱们能够分三种状况讨论,先肯定中间的8个数字的值,假设为1的话,2和3就能够当作是同样的,因而状态数变成了2^24。
对三种状况分别进行迭代加深搜索,令当前须要搜索的中间8个数字为k,首先枚举本次搜索的最大深度maxDepth(即须要的步数),从初始状态进行状态扩展,每次扩展8个结点,当搜索到深度为depth的时候,那么剩下能够移动的步数为maxDepth - depth,咱们发现每次移动,中间的8个格子最多多一个k,因此若是当前状态下中间8个格子有sum个k,那么须要的剩余步数的理想最小值s = 8 - sum,那么估价函数:
h = depth + (8 - sum)
当h >
maxDepth时,代表在当前这种状态下,不可能在
maxDepth歩之内达成目标,直接回溯
。
当某个深度
maxDepth至少有一个可行解时,整个算法也就结束了,能够设定一个标记,直接回溯到最上层,或者在DFS的返回值给定,对于某个搜索树,只要该子树下有解就返回1,不然返回0。
迭代加深适合深度不是很深,可是每次扩展的结点数不少的搜索问题。
二、广度优先搜索
一、BFS
1) 算法原理
广度优先搜索即Breadth First Search,也是图遍历算法的一种。用一句话归纳就是:“我会分身我怕谁?!”。
BFS的具体算法描述为选择一个起始点v放入一个先进先出的队列中,执行以下操做:
a. 若是队列不为空,弹出一个队列首元素,记为
当前结点,
执行b;不然算法结束;
b. 将与
当前结点
相邻而且还没有被访问的结点的信息进行更新,而且所有放入队列中,继续执行a;
维护广搜的数据结构是队列和HASH,队列就是官方所说的open-close表,HASH主要是用来标记状态的,好比某个状态并非一个整数,多是一个字符串,就须要用字符串映射到一个整数,能够本身写个散列HASH表,不建议用STL的map,效率奇低。
广搜最基础的应用是用来求图的最短路。
如图9所示,对如下图进行广度优先搜索,假设起点为1,将它放入队列后。那么第一次从队列中弹出的必定是1,将和1相邻未被访问的结点继续按顺序放入队列中,分别是二、三、四、五、7,而且记录下它们距离起点的距离dis[x] = dis[1] + 1 (x 属于集合 {2, 3, 4, 5, 7});而后弹出的元素是2,和2相邻未被访问的结点是10,将它也放入队列中,记录dis[10] = dis[2] + 1;而后弹出5,放入6(4因为已经被访问过,因此不须要再放入队列中);弹出7,放入八、9。队列为空后结束搜索,搜索完毕后,dis数组就记录了起点1到各个点的最短距离;
2) 算法实现
广搜通常用队列维护状态,写成伪代码以下:
def BFS(v):
resetArray(visited,
false)
visited[v] =
true
queue.push(v)
while not queue.empty():
v = queue.getfront_and_pop()
for u
in adjcent_list[v]:
if visited[u]
is
false:
dosomething(u)
queue.push(u)
3) 基础应用
a. 最短路:
bellman-ford最短路的优化算法SPFA,主体是利用BFS实现的。
绝大部分四向、八向迷宫的最短路问题。
b. 拓扑排序:
首先找入度为0的点入队,弹出元素执行“减度”操做,继续将减完度后入度为0的点入队,循环操做,直到队列为空,经典BFS操做;
c. FloodFill:
经典洪水灌溉算法;
4) 高级应用
a. 差分约束:
数形结合的经典算法,利用SPFA来求解不等式组。
b. 稳定婚姻:
二分图的稳定匹配问题,试问没有稳定的婚姻,如何有心思学习算法,因此必定要学好BFS啊;
c
. AC自动机:
字典树 + KMP + BFS,在设定失败指针的时候须要用到BFS。
d. 矩阵二分:
矩阵乘法的状态转移图的构建能够采用BFS;
e
. 基于k进制的状态压缩搜索:
这里的k通常为2的幂,状态压缩就是将本来多维的状态压缩到一个k进制的整数中,便于存储在一个一维数组中,每每能够大大地节省空间,又因为k为2的幂,因此状态转移能够采用位运算进行加速,HDU1813和HDU3278以及HDU3900都是很好的例子;
f
. 其它:
还有好多,一时间想不起来了,占坑;
二、基于BFS的A*
1) 算法原理
在搜索的时候,结点信息要用堆(优先队列)维护大小,即能更快到达目标的结点优先弹出。
2) 基础应用
a.
八数码问题
如图10所示,一个3*3的棋盘,放置8个棋子,编号1-8,给定任意一个初始状态,每次能够交换相邻两个棋子的位置,问最少通过多少次交换使棋盘有序。
图10
遇到搜索问题通常都是先分析状态,这题的状态数能够这么考虑:将数字1放在九个格子中的任意一个,那么数字2有八种摆放方式,3有七种,依此类推;因此状态总数为9的排列数,即9!(9的阶乘) = 362880。每一个状态能够映射到0到362880-1的一个整数,
对于广搜来讲这个状态量不算大,可是也不小,若是遇到无解的状况,就会把全部状态搜遍,因此这里必须先将无解的状况进行特判,采用的是曼哈顿距离和逆序数进行剪枝,具体参见 SGU 139的解法:
网上对A*的描述写的都很复杂,我尝试用个人理解简单描述一下,首先仍是从公式入手:
f(state) = g(state) + h(state)
g(state) 表示从初始状态 到 state 的实际行走步数,这个是经过BFS进行实时记录的,是一个已知量;
h(state) 表示从 state 到 目标状态 的指望步数,这个是一个估计值,不能准确获得,只能经过一些方法估计出一个值,并不许确;
f(state) 表示从 初始状态 到 目标状态 的指望步数,这个没什么好说的,就是前两个数相加获得,也确定是个估计值;
对于广搜的状态,咱们是用队列来维护的,因此state都是存在于队列中的,咱们但愿队列中状态的
f(state)值是单调不降的(这样才能尽可能早得搜到一个解),
g(state)能够在状态扩展的时候由当前状态的父状态pstate的
g(pstate)+1获得;那么问题就在于
h(state),用什么来做为state的指望步数,这个对于每一个问题都是不同的,在八数码问题中,咱们能够这样想:
这个棋盘上每一个有数字的格子都住了一位老爷爷 (-_-|||),每位老爷爷都想回家,老爷爷的家就对应了目标状态每一个数字所在的位置,对于 i 号老爷爷,他要回家的话至少要走的路程为当前状态state它在的格子pos[i] 和 目标状态他的家target[i] 的曼哈顿距离。每位老爷爷都要回家,因此最少的回家距离就是全部的这些曼哈顿距离之和,这就是咱们在state状态要到达目标状态的指望步数
h(state),不理解请回到两行前再读一遍或者看下面的公式。
h(state) = sum( abs(pos[i].x - (i-1)/3) + abs(pos[i].y - (i-1)%3) ) (其中 1 <= i <= 8, 0 <= pos[i].x, pos[i].y < 3 )
b.K短路问题
求初始结点到目标结点的第K短路,当K=1时,即最短路问题,K=2时,则为次短路问题,当K >= 3时须要A*求解。
仍是一个h(state)函数,这里能够采用state到目标结点的最短距离为指望距离;
三、双向广搜
1) 算法原理
初始状态 和 目标状态 都知道,求初始状态到目标状态的最短距离;
利用两个队列,初始化时初始状态在1号队列里,目标状态在2号队列里,而且记录这两个状态的层次都为0,而后分别执行以下操做:
a.
若1号队列已空,则结束搜索,不然
从1号队列逐个弹出层次为K(K >= 0)的状态;
i. 若是该状态在2号队列扩展状态时已经扩展到过,那么最短距离为两个队列扩展状态的层次加和
,结束搜索
;
ii. 不然和BFS同样扩展状态,放入1号队列,直到队列首元素的层次为K+1时执行b;
b.
若2号队列已空,则结束搜索,
不然
从2号队列逐个弹出层次为K(K >= 0)的状态;
i. 若是该状态在1
号队列扩展状态时已经扩展到过,那么最短距离为两个队列扩展状态的层次加和
,结束搜索
;
ii. 不然和BFS同样扩展状态,放入
2号队列,直到队列首元素的层次为K+1时执行a;
如图11,S表示初始状态,T表示目标状态,红色路径链接的点为S扩展出来的,蓝色路径链接的点为T扩展出来的,当S扩展到第三层的时候发现有一个结点已经在T扩展出来的集合中,因而搜索结束,最短距离等于3 + 2 = 5;
双广的思想很简单,本身写上一两个基本上就能总结出固定套路了,和BFS同样属于盲搜。
3、搜索题集整理
二、IDA* (肯定是迭代加深后就一个套路,枚举深度,而后 暴力搜索+强剪枝)
三、BFS
Puzzle ★★★★★ 几乎尝试了全部的搜索 -_-||| 让人欲仙欲死的题
四、双向BFS
(适用于起始状态都给定的问题,通常一眼就能看出来,固定套路,很难有好的剪枝)
人一我百!人十我万!永不放弃~~~怀着自信的心,去追逐梦想。