在经历的期中考试和一个快乐五一之后,还是要开始干活了。
这次学习主要是与简单搜索技术相关的总结。
(一).递归与全排列
下面是有关全排列的两个问题。
1.打印从n个数中任意m个数的全排列(这里以1~9的全排列为例)
这个题目,书上给出了两种方法:
第一种方法是用暴力法遍历,因为全排列中不存在重复序列,直接用暴力法循环遍历9次即可,当然,这个方法过于笨拙,但简单有效。
第二种方法则是使用递归,第一层递归是有关9个序列的,每次改变第一个数,这样每个序列就是不同的,再然后在第一层递归后,进行第二层递归,现在不看第一个数,把第二个数当成第一个数,重复上述操作,依次类推即可。下面是代码的复现:
这个问题实质上与求字典序类似。
2.打印n个数中任意m个数的组合
要注意,这个问题和第一个问题是有区别的,因为排列是有序的,组合是无序的,所以应该对重复元素去重是这个问题的关键。当然,这个题目也可以根据set容器的去重特性得到解决。这里我们使用另外一种思路:即把每个子集对应一个二进制数,则k个元素的集合,它对应二进制数中就有k个1。这个问题就转化为查找1的个数为k的二进制数即可。2^k(子集数) = 2^(k-1 )+2^(k-2)...+2^0+1(对应二进制数的个数)。而查找一个位串中k的个数,在离散数学中,我们学到过这个方法:可以使用按位与,即kk = kk&(kk-1),它只需要执行1的个数次这个操作即可。
(二).BFS(Breath-First Search,广度优先搜索)
用书上的一个比喻来解释BFS:一群老鼠走迷宫,假定老鼠无限多,每个路口派出老鼠探索所有没有走过的路,如果碰壁或者走到其他老鼠走过的路,就停下。显然,所有的道路都会被走到,那么这个问题就一定可以得到解决。所有该方法具有可行性。BFS可以看成是并行计算的模拟,通常用队列这种数据结构来实现BFS。下面用BFS来解决典型的状态图搜索——八数码问题。问题描述如下:(盗图,嘿嘿)
八数码问题从初始状态出发,每次转移都逐渐逼近目标,是从近到远的扩散过程。BFS比较适合运用于最短路径问题。这个问题理解起来很容易,但是会有一个问题:这样搜索下去,其实有很多路径是重复了的,这加大了不必要的计算成本。因此,八数码问题的关键在于去重。这里我们使用康托展开去重。康托展开简单来讲就是一个全排列所对应的字典序。此外,康托展开也是一个数组到一个数的映射,因此也是可用于hash,用于空间压缩。比如在保存一个序列,我们可能需要开一个数组,如果能够把它映射成一个自然数, 则只需要保存一个整数,大大压缩空间。比如八数码问题。
下面是这个问题的解决方案。
当然,就这个问题而言,仅有9! = 36288种情况,但是对于情况更为复杂的棋盘的问题,这种方法显然是不够的。所以这种BFS+Cantor的算法是存在着一定的局限性的。
当然,对于这个题可以使用双向BFS,即从目标状态和初始状态分别使用BFS,如果出现交集则问题有解。
(三).DFS(Depth-First Search,深度优先搜索)
用一个形象的说法解释DFS,就是一只老鼠走迷宫,一直走到不能继续往下走为止,走过的位置不在走,不能走时则回退一步,再换个方向走。这样每个路口可以视作几乎没有重复访问。DFS代码通常比BFS更为简单,用于求解可行解较为合适,BFS可用来求解最优解(如最短路径问题)。DFS常用递归来实现。
(四).回溯和剪枝
在使用DFS算法时,经常会遇到不符合要求的递归,在某一个条件不成立时,就应该立刻停止递归,以免增大不必要的计算,再然后返回到上一个状态,这个过程就是回溯。而判断递归是否符合要求,则可以使用剪枝函数。例如八皇后问题,要求皇后不能同行,同列,同对角线。如果递归到某一个后和另一个后同处一行,一列或者对角线时,递归就不必往下进行。剪枝函数可以对此进行判断,如:设定左上角为(0,0),已放好的皇后为(i,j),新后的坐标为(m,n),则:
比如两后不能同对角:所以|i-m| ≠ |j-n|。