510 likes | 646 Views
搜索初步. 什么是搜索. 搜索算法是算法设计中一种基本的方法。当解决某个问题的时候,没有(或一时找不到)一个行之有效的方法,我们通常可以使用“搜索”。将所有情况一一罗列出来并加以判断(或将问题的全部解列出并一一验证),从而找出所需求解问题的解(或解集)。. 搜索的常用算法. 穷举 回溯 深度优先搜索 广度优先搜索. 递归. 由于在穷举、回溯、深度优先搜索等搜索方法中经常会用到递归,我们先来回顾一下递归的相关知识。 若在一个函数、过程或者数据结构定义的内部,直接(或间接)出现定义本身的应用,则称它们是递归的,或者是递归定义的。 n! = n * (n-1)!. 递归.
E N D
什么是搜索 • 搜索算法是算法设计中一种基本的方法。当解决某个问题的时候,没有(或一时找不到)一个行之有效的方法,我们通常可以使用“搜索”。将所有情况一一罗列出来并加以判断(或将问题的全部解列出并一一验证),从而找出所需求解问题的解(或解集)。
搜索的常用算法 • 穷举 • 回溯 • 深度优先搜索 • 广度优先搜索
递归 • 由于在穷举、回溯、深度优先搜索等搜索方法中经常会用到递归,我们先来回顾一下递归的相关知识。 • 若在一个函数、过程或者数据结构定义的内部,直接(或间接)出现定义本身的应用,则称它们是递归的,或者是递归定义的。 • n! = n * (n-1)!
递归 • 对于一个递归定义而已,除了要定义递归的方式(即如何递归)外,还必须定义递归的终止条件(即如何停止递归),否则递归将永无止境的进行下去。
递归算法用于解决的问题 • 一般来说,能够用递归解决的问题应该满足以下三个条件: • 需要解决的问题可以化为一个或多个子问题来求解,而这些子问题的求解方法与原来的问题完全相同,只是在数量规模上不同; • 递归调用的次数必须是有限的; • 必须有结束递归的条件(边界条件)来终止递归。
递归算法用于解决的问题 • 可用递归解决的具体问题一般包括: • 数据的定义形式是按递归定义的。如阶乘。 • 问题的解法按递归算法实现。如回溯算法。 • 数据结构的形式是按递归定义的。如树的遍历。 procedure m-search(t: node)begin if (t <> nil) then begin m-search(t.left); visit(t); m-search(l.right); end; end; function jc(n: integer): longint; begin if (n = 0) then jc := 1 else jc := n * jc(n-1); end;
递归算法的执行过程 • 在递归调用之前,系统需完成三件事: • 为被调用过程的局部变量分配存储区; • 将所有的实在参数、返回地址等信息传递给被调用过程保存; • 将控制转移到被调过程的入口。 • 从被调用过程返回调用过程之前,系统也应完成三件工作: • 保存被调过程的计算结果; • 释放被调过程的数据区; • 依照被调过程保存的返回地址将控制转移到调用过程。
递归算法的执行过程 • 在计算机中,是通过使用系统栈来完成上述操作的。系统栈的元素会包括值参、局部变量和返回地址。在每次执行递归调用之前,系统自动把本程序所使用到的值参和局部变量的当前值以及调用后的返回地址压栈(保存现场);当每次递归调用结束之后,系统又自动把栈顶元素出栈,覆盖掉相应的值参和局部变量,使他们恢复到递归调用之前的值(恢复现场),然后程序无条件的转向由返回地址所指定的位置继续执行。
=120 =24 =6 =2 =1 =1 递归算法的执行过程 1:function jc(n: integer): longint;2:begin3: if (n = 0) then4: jc := 15: else6: jc := n * jc(n-1);7:end; 主程序 n=1,retaddr=6 Fac(5) n*Fac(4) n=2,retaddr=6 n*Fac(3) n=3,retaddr=6 n*Fac(2) n=4,retaddr=6 n*Fac(1) n=5,retaddr=6 n*Fac(0) 1
优点 能用有限的语句来定义对象的无限集合; 简化某些复杂问题的处理过程; 代码的可读性较好。 缺点 系统开销大,程序的运行效率比较低; 递归的深度受到系统栈空间大小的限制。 递归算法的优缺点
穷举搜索 • 穷举策略基于计算机运算速度快的特点,在一时找不到解决问题的更好途径(如求解公式或规则)时,根据问题的部分条件(约束条件)将所有可能解的情况列举出来,然后一一验证是否符合整个问题的求解要求。
穷举搜索 • 在很多问题中,穷举的数量级往往是阶乘级或指数级的。即使高速计算机也很难在可以忍受的时间内给出问题的解。因此在穷举时需要尽可能的将一些明显不合题意的情况排除,以减少穷举的数量级。
n皇后问题 • 在一个n×n的棋盘上放置n个国际象棋中的皇后,要求所有的皇后之间都不形成攻击。请你给出所有可能的排布方案数。 • 输入:4 • 输出:2
n皇后问题 • 对于n皇后问题而言,我们很难找出很合适的方法来快速的得到解,因此我们采取最基本的枚举法来求解。 • 我们知道,在n×n的棋盘上放置n个棋子的所有放置方案有 种,随着n的增大,而这个数字会变得非常庞大,直接枚举肯定会超时。
n皇后问题 • 考虑到皇后攻击的特性,所有的皇后不能同行、同列,因此,我们可以人为的限定所有的皇后不同行、同列,这样的话,所有的可能性就只有n!种了。 • 我们只要枚举出所有这n!种排布,找出其中满足任意两皇后都不共对角线的所有情况即可。
n皇后问题 • 那么如何来枚举这n!种排布呢? • 既然所有的皇后都不能同行或同列,那么不妨我们先人为规定第k个皇后在第k行,这样的话我们只要给出每个皇后所处的列号就可以描述皇后的位置了。如图放置方式就可以被表示为:3 1 4 2即第1个皇后在第3列,第2个皇后在第1列,…… • 因此求这n!种排布就被转为求1~n的全排列
回溯算法 • 像走迷宫这样,遇到死路就回头的搜索思路就叫做“回溯”。 • 从问题的某种可能情况出发,搜索所有能到达的可能情况,然后以其中一种可能的情况为新的出发点,继续向下探索,当所有可能情况都探索过且都无法到达目标的时候,再回退到上一个出发点,继续探索另一个可能情况,这种不断回头寻找目标的方法称为“回溯法”。
n皇后问题 • n皇后虽然可以用穷举来实现,但仅适用于n较小的情况,当n逐渐增大后,n!的增长速度是很惊人的。 • 我们来看看如何继续进行优化。
n皇后问题 • 我们现在不一下子把所有n个皇后都放到棋盘上,我们改为一个一个放。 • 如果某个皇后放到棋盘上后和棋盘上前面的皇后发生冲突,就不用再试放后面的皇后了; • 如果某个皇后可放置的所有列都发生冲突,则回去重新试放前一个皇后。
n皇后问题 • 回溯算法通常用递归实现 procedure search(k: integer); var i: integer; begin if k > n then // 是否前n个皇后都已经放下 [找到一组解,做相应处理] else // 还有皇后没放 for i := 1 to n do begin // 从第1列开始逐列尝试 x[k] := i; // 把第k个皇后放在第i列 if check(k) then // 第k个皇后可否放在第i列 search(k + 1); // 继续试放第k+1个皇后 end; end;
回溯的一般步骤 • 首先需要为问题定义一个解空间(solution space),这个空间必须至少包含问题的一个解; • 然后我们需要组织解空间,以便它能被容易地搜索。典型的组织方法是图或树。 • 最后对这个空间按深度优先的方法从开始节点进行搜索。利用限界函数避免移动到不可能产生解的子空间。
n皇后问题 • n皇后问题的解空间是1~n全排列的一部分。 • 解空间的长度是n。 • 解空间的组织形式是一棵n叉树,一个可行的解就是从根节点到叶子节点的一条路径。 • 控制策略则是当前皇后与前面所有的皇后都不同列和不同对角线。
回溯的一些特点 • 搜索策略:符合递归的思路,采用深度优先搜索; • 控制策略:在搜索过程中如遇到冲突,则立即放弃此后的搜索,返回上一层继续搜索; • 数据结构:常用栈来保存搜索过程中的状态和路径,所需空间大小为搜索所需最长路径的长度。 • 在搜索执行的同时产生解空间。在搜索期间的任何时刻,仅保留从开始节点到当前扩展节点的路径。
深度、宽度优先搜索 • 在搜索过程中,我们把每个状态看作是结点,把状态之间的联系看做是边,这样我们就可以得到一棵树,我们把这棵树称为“搜索树”。
深度、宽度优先搜索 • 初始状态对应根结点,目标状态对应目标结点。问题的一个解就是一条从根结点到目标结点的路径。 • 对“搜索树”的搜索算法类似于树的遍历,通常有两种不同的实现方法: • 广度优先搜索(BFS) • 深度优先搜索(DFS)
广度优先搜索 • BFS每次都先将搜索树某一层的所有节点全部访问完毕后再访问下一层,因此也被称作“按层搜索”。
Data OP Pre 广度优先搜索 • 一般来说,BFS使用队列来实现。 • BFS一般用来求最优解。 • 在存储数据时,除了需要存储当前状态外,还需要存储当前状态的父状态以及由父状态转换过来所执行的操作。
广度优先搜索算法 • 初始状态入队 • op := 1 • 对队首状态进行操作op,得到新状态; • 检查此状态是否出现过,如未出现则将此状态入队; • 如果此状态为目标状态,则输出; • 如所有操作都已完成,则队首出队,否则op := op + 1,返回(3)。
广度优先搜索的程序框架 procedure bfs; begin head := 0; tail := 1; data[tail].data := 初始状态; data[tail].op := 0; data[tail].pre := 0; flag := false; repeat inc(head); while data[head]还可以扩展 do begin new := op(data[head].state); if new已经出现 then continue; inc(tail); data[tail].data := new; data[tail].op := op; data[tail].pre := head; if new是目标状态 then begin flag := true; break; end; end; until (tail = head) or flag if flag then output else writeln('No Answer'); end;
深度优先搜索 • DFS总是能尽可能快地抵达搜索树的底层,用逐步加层的方法搜索并且适当时回溯,在每一个已被访问过的节点上标号,以便下次回溯时不会再次被搜索。 • 在搜索过程中,对于当前发现的节点,如果它还存在以它为起点而未探测到的边时,就沿此边继续搜索下去。若它所有的边都已被探测过,则回溯到它的父节点继续搜索的过程,直到找到目标节点或探测完所有的节点。 • 在具体实现时,通常用递归或栈来实现。
深度优先搜索算法 • 从初始结点开始,将待扩展结点入栈; • 如果栈空,则表示无解或已全部扩展完,退出; • 否则将栈顶元素出栈,并扩展出所有的子结点并依次入栈。若无子结点可扩展则转(2); • 如果某子结点是目标结点,则找到一个解。如果要求所有解或最优解则转(2),否则结束。
深度优先搜索的程序框架 procedure dfs; begin top := 1; s[top].data := 初始状态; flag := false; repeat curr := s[top]; dec(top); while curr还可以扩展 do begin new := op(curr.data); if new已经出现 then continue; inc(top); s[top].data := new; s[top].pre := curr; if new是目标状态 then begin flag := true; break; end; end; until (top < 1) or flag if flag then output else writeln('No Answer'); end;
深度优先算法 • 在实际编程过程中,我们常常适用递归来实现DFS,使用“做标记”的方法来取代保存状态的操作,这样处理以后的DFS就和回溯非常的相像了。
例题 • 迷宫问题:给出一个n×m的迷宫,请你给出一条从入口(1,1)到出口(n,m)的路径。 • 跳马问题:在n×n的棋盘上有一象棋中的马,马的起始位置已知,请你给出一个方案,使马能周游棋盘上每一个格子(每个格子只能走一次)。
例题 • 迷宫问题:给出一个n×m的迷宫,请你给出一条从入口(1,1)到出口(n,m)的最短路径。 • 倒油问题:有一个一斤的瓶子装满油,另有一个七两和一个三两的空瓶,再没有其它工具。只用这三个瓶子怎样精确地把一斤油分成两个半斤。
DFS or BFS k表示树的深度;d表示解的深度;d≤k
深度优先搜索的优化 • 由于DFS本质上就是穷举,因此其时间复杂度相当高,在实际应用中通常需要对其进行优化。 • 常用的优化方法有以下三种: • 缩小搜索范围; • 改变搜索次序; • 剪枝。
深度优先搜索的优化 • 缩小搜索范围 • 在递归前对尚待搜索的信息进行预处理,减少搜索量; • 增加约束条件,使其在保证不遗漏解的前提下尽可能的苛刻。
深度优先搜索的优化 • 比如在跳马问题中,除了前面的约束条件外,还有更强的约束条件。 • “孤点”和“终点”
深度优先搜索的优化 • 改变搜索的次序 • 如果要求问题的全部解,改变搜索次序没有任何意义,但如果只要得到问题的一个解时,通过改变搜索的次序则可能得到意想不到的效果。
深度优先搜索的优化 • 在跳马问题中, 马可以往八个方向跳跃,通常情况下,我们都是按照方向依次尝试,但这样就比较容易走进死胡同,从而需要大量的时间进行回溯。 • 事实上,我们可以让马跳得更智能些。
深度优先搜索的优化 • 比如从1可以到2或22,而从2可以到3、7、9、19、21;从22可以到11、17、21、15、23。都是5种选择,我们随便选其中一个。 • 若选择2,则从3、7、21、19、9出发的目的地有1、3、3、3、3个。 • 那么该选哪个呢?
深度优先搜索的优化 • 我们选择目的地数最少的格子。 • 这样选择可以使得回溯的时候能一下子回到第2步,大大缩短回溯的时间,同时,这样的跳法可以使马尽量先往边角处跳,也就不容易产生“孤点”和“终点”。
深度优先搜索的优化 • 剪枝 • 通过某种判断,避免一些不必要的遍历过程。 • 剪枝时必须注意以下四个方面: • 正确性:防止剪掉包含正确解的分支。这是剪枝优化的前提,一般通过“必要条件”来剪枝。 • 力度:直接影响搜索的效率。 • 代价:处理好剪枝力度和剪枝判断的复杂度。 • 剪枝条件的获得方法:直觉法和推理法。
广度优先搜索的优化 • 对于BFS而言,当求解步骤较长时,由于需要存储全部的扩展结点,很容易造成空间不够的情况。 • 我们一般可以采用如下方法进行优化: • 双向BFS • 迭代+DFS
广度优先搜索的优化 • 双向BFS • 从起始状态和目标状态同时进行BFS,这样,各自只要扩展原来一半的层数,因此空间大大减少。
广度优先搜索的优化 • 迭代+DFS • 迭代加深搜索实质是限定下界的深度优先搜索,即首先允许深度优先搜索搜索 k 层搜索树,若没有发现可行解,再将 k+1 后再进行一次以上步骤,直到搜索到可行解。
广度优先搜索的优化 • 已知有两个字串 A$, B$ 及一组字串变换的规则(至多6个规则):A1$B1$,A2$B2$,…。我们可以将A$中的A1$替换成B1$,现请你编程计算使A$变换成B$最少需要执行多少次变换操作。