第3章图搜索算法 搜索算法的搜索路径是用称为图的数据结构保存的。图由节点和边构成,节点代表搜索过程中的某一个状态,边是从一个节点过渡到另一个节点的算子。前面讨论了用搜索求解问题的方法是在一个图(一般是隐式图)中进行搜索,而搜索的策略是在图中寻找最佳解路径。下面首先讨论图在搜索过程中是以怎样的形式存储的。 抽象地来看,搜索图可以有两种结构。 (1) 树结构,允许搜索图中有相同状态多次出现,每出现一次就用一个新的节点表示,这样的图没有回路出现,即没有圈。 (2) 图结构,不允许搜索图中有相同状态多次出现,即相同的状态只能用一个节点表示。这样,如果有两条路径都生成了某个状态,这个状态只能用一个节点表示,这就在图中形成了圈。 本章讨论用图来存储问题的搜索空间,而保存解路径则用树结构。根据图对应的实际问题背景可分为“或图”和“与/或图”两种。或图对应的背景为搜索扩展时,可在若干分支中选择一个(这就是“或”的意思); 与/或图则是在搜索扩展时,有可能同时搜索若干分支(这就是“与”的含义),也有可能在若干分支选择其中之一(这就是“或”的含义)。这两种图都可能存在回路,以下讨论的算法中,都通过一定的手段避开在回路中循环搜索。 3.1或图搜索 或图对应的背景为搜索扩展时,可在若干分支中选择一个。例如编写一个下棋程序,其搜索过程的每一步就是在若干下棋的走步中选择一个,这就是典型的或图中的搜索。 下面首先讨论一般或图上的搜索算法,然后讨论建立在这个一般算法之上的若干更高效的搜索算法。 3.1.1或图搜索算法 图搜索算法只记录状态空间那些被搜索过的状态,它们组成一个搜索图G。G由两张存放节点的表(List),再加上一个反向树组成。 (1) Open表。用于存放已经生成,且已用启发式函数作过估计或评价,但尚未产生它们的后继节点的那些节点,也称未考察节点。 (2) Closed表。用于存放已经生成,且已考察过的节点。 (3) 反向边组成的Tree。用来存放当前已生成的搜索树,该树也是一个表,该表的元素是搜索过程中的反向边(反向指针)。 例3.1在如图3.1所示的搜索图中,假如由节点A搜索到节点B,再搜索到节点E,而且已扩展了它的两个后继节点。此时用上述方法表示的隐式图G为: Open=(D,H,I)//还未考察节点 Closed=(A,B,E)//已考察过的节点 Tree=((E,B),(B,A)) //反向边组成的表 图3.1一个简单的搜索图 图3.1中,假如I是目标节点,搜索到I节点后,搜索成功,(I,E)加入到了Tree。这时,根据这个Tree,做反向查找,很快就可以找到解路径是(A,B)(B,E)(E,I)。 下面讨论或图通用搜索算法,通用的含义是,只要搜索问题能表示成这样一个或图,这个算法就可以用于求解这个问题,即算法具有通用性。 1. 或图通用搜索算法 设S0为初始状态,Sg为目标状态。 (1) 产生仅由S0组成的Open表,即Open=(S0)。 (2) 产生一个空的Closed表。 (3) 如果Open为空,则失败退出。 (4) 在Open表上按某一原则选出一个节点,称为n,将n放到Closed表中,并从Open表中去掉n。 (5) 若n∈Sg,则成功退出,此时,解为在Tree中沿指针从n到S0的路径,或n本身。(例如八皇后问题给出到达的目标状态n即可,八数码问题要给出到达目标状态的路径)。 (6) 产生n的一切后继,将后继中不是n的先辈点的一切点构成集合M,装入G作为n的后继。(这就剔除了既是n的先辈又是n的后继的结点,从而避免了回路。) (7) 对M中的元素P,分别作两类处理: ① 若PG,即P不在Open表中也不在Closed表中,则P根据一定原则加入Open表,同时对P进行评价,把指向P的边反向后,加入Tree中。 ② 若P∈G,则决定是否更改Tree中P到n的指针。 (8) 转3。 2. 算法说明 以上算法中有两点需要说明。 说明一: 在算法的第(4)步,每次取出Open表的第一个节点,然后在第(7)步的①中,若生成的后继节点放于: (1) Open表的尾部,算法相当于广度优先(Breadthfirstsearch)。 (2) Open表的首部,算法相当于深度优先(Depthfirstsearch)。 (3) 根据启发式函数f的估计值确定最佳者,放于Open表的首部,算法相当于最佳优先(Bestfirstsearch)。 例3.2我们不妨对上述第(1)种情况进行算法步骤的跟踪,还是假定目标是图3.1中的节点G。 (1) 产生仅由S0组成的Open表,即Open=(A)。 (2) 产生一个空的Closed表。 (3) Open=(A),不为空。 (4) 从Open表中取出第一个节点A,称为n,将n放到Closed表中,并从Open表中去掉A,此时Open=( ),Closed=(A)。 (5) n=A,不是目标。 (6) 产生A的后继B和C,此时M=(B,C)。 (7) 考察M中的元素B、C,它们不在Open表中也不在Closed表中,加入Open表,Open=(B,C), 再将反向边(B,A)、(C,A)加入Tree中。 (8) 转(3)。 (9) Open=(B,C),不为空。 (10) 从Open表中取出第一个结点B,放到Closed表中,并从Open表中去掉B,此时Open=(C),Closed=(A,B)。 (11) n=B,不是目标。 (12) 产生B的后继D、E,此时M=(D,E)。 (13) 考察M中的元素D、E,它们不在Open表中也不在Closed表中,加入Open表,Open=(C,D,E), 反向边(D,B)、(E,B)加入Tree中。 (14) 转(3)。 (15) Open=(C,D,E),不为空。 (16) 从Open表中取出第一个节点C,放到Closed表中,并从Open表中去掉C,此时Open=(D,E),Closed=(A,B,C)。 (17) n=C,不是目标。 (18) 产生C的后继F、G,此时M=(F,G)。 (19) 考察M中的元素F、G,它们不在Open表中也不在Closed表中,加入Open表,Open=(D,E,F,G), 反向边(F,C)、(G,C)加入Tree中。 (20) 转(3)。 继续跟踪下去,不难看出,按照这种策略,访问的次序是A、B、C、D、E、F、G。这正是广度优先搜索的次序。 若生成的后继节点放于Open表的首部,读者用上述同样的步骤进行跟踪,不难看出,访问的次序是A、B、D、E、H、I、C、F、G。这正是深度优先搜索的次序。 若将启发式函数f的估计值的最佳者放于Open表的首部,再对或图通用的搜索算法进行跟踪。对于图3.1,若将达到某个节点的边的权值作为这个节点启发式函数f的值,即f(B)=3,f(C)=1,f(D)=4,f(E)=5,f(F)=3,f(G)=7,f(H)=3,f(I)=2,在这个假定下,搜索算法访问的次序分别是A、C、B、E、H、I、D、F、G。读者不妨跟踪算法,记录一下Open表中节点的编号变化的情况。 说明二: 在算法的第②步中,若p∈G,则决定是否更改Tree中p到n的指针。这是因为p已经在G中,说明以前访问过p节点,那么原来的访问路径与当前的访问路径就要进行比较,看哪一条路径更好。 例如,如图3.2所示,p∈M且在Open表中,这说明p在作为n的后继之前已是某一节点m的后继,但本身尚未被考察(未生成p的后继)。 图3.2p在n之前已是某一节点m的后继 这说明S0→p至少有两条路径,这时有两种情况:  若Path1的代价