使用计算机求解的问题中,有许多问题是无法用数学公式进行计算推导采用模拟方法来找出答案的。这样的问题往往需要我们根据问题所给定的一些条件,在问题的所有可能解中用某种方式找出问题的解来,这就是所谓的搜索法或搜索技术。
通常用搜索技术解决的问题可以分成两类:一类问题是给定初始结点,要求找出符合约束条件的目标结点;另一类问题是给出初始结点和目标结点,找出一条从初始结点到达目标结点的路径。
常见的搜索算法有枚举法、广度优先搜索法、深度优先搜索法、双向广度优先搜索法,A*算法、回溯法、分支定界法等。这里来讨论一下广度优先搜索法。
一.广度优先搜索算法
1. 问题的特征
可以采用搜索算法解决的这类问题的特点是:
1)有一组具体的状态,状态是问题可能出现的每一种情况。全体状态所构成的状态空间是有限的,问题规模较小。
2)在问题的解答过程中,可以从一个状态按照问题给定的条件,转变为另外的一个或几个状态。
3)可以判断一个状态的合法性,并且有明确的一个或多个目标状态。
4)所要解决的问题是:根据给定的初始状态找出目标状态,或根据给定的初始状态和结束状态,找出一条从初始状态到结束状态的路径。
2.广度优先搜索算法解题的步骤
1)定义一个状态结点
采用广度优先搜索算法解答问题时,需要构造一个表明状态特征和不同状态之间关系的数据结构,这种数据结构称为结点。不同的问题需要用不同的数据结构描述。
2)确定结点的扩展规则
根据问题所给定的条件,从一个结点出发,可以生成一个或多个新的结点,这个过程通常称为扩展。结点之间的关系一般可以表示成一棵树,它被称为解答树。搜索算法的搜索过程实际上就是根据初始条件和扩展规则构造一棵解答树并寻找符合目标状态的结点的过程。
广度优先搜索算法中,解答树上结点的扩展是沿结点深度的“断层”进行,也就是说,结点的扩展是按它们接近起始结点的程度依次进行的。首先生成第一层结点,同时检查目标结点是否在所生成的结点中,如果不在,则将所有的第一层结点逐一扩展,得到第二层结点,并检查第二层结点是否包含目标结点,...对长度为n+1的任一结点进行扩展之前,必须先考虑长度为n的结点的每种可能的状态。因此,对于同一层结点来说,求解问题的价值是相同的,我们可以按任意顺序来扩展它们。这里采用的原则是先生成的结点先扩展。
结点的扩展规则也就是如何从现有的结点生成新结点。对不同的问题,结点的扩展规则也不相同,需要按照问题的要求确定。
3)搜索策略
为了便于进行搜索,要设置一个表存储所有的结点。因为在广度优先搜索算法中,要满足先生成的结点先扩展的原则,所以存储结点的表一般设计成队列的数据结构。
搜索的步骤一般是:
(1)从队列头取出一个结点,检查它按照扩展规则是否能够扩展,如果能则产生一个新结点。
(2)检查新生成的结点,看它是否已在队列中存在,如果新结点已经在队列中出现过,就放弃这个结点,然后回到第(1)步。否则,如果新结点未曾在队列中出现过,则将它加入到队列尾。
(3)检查新结点是否目标结点。如果新结点是目标结点,则搜索成功,程序结束;若新结点不是目标结点,则回到第(1)步,再从队列头取出结点进行扩展......。
最终可能产生两种结果:找到目标结点,或扩展完所有结点而没有找到目标结点。
如果目标结点存在于解答树的有限层上,广度优先搜索算法一定能保证找到一条通向它的最佳路径,因此广度优先搜索算法特别适用于只需求出最优解的问题。当问题需要给出解的路径,则要保存每个结点的来源,也就是它是从哪一个节点扩展来的。
3.广度优先搜索算法的算法框架
对于广度优先搜索法来说,问题不同则状态结点的结构和结点扩展规则是不同的,但搜索的策略是相同的,因此算法框架也基本相同。
struct tnode{ //定义一个结点数据类型
.... //根据具体问题确定所需的数据类型
}state[maxn]; //定义tnode类型的数组作为存储结点的队列
void init(); //初始化函数
bool extend(); //判断结点是否能扩展,如果能则产生新结点
bool repeat(); //检查新结点是否在队列中已经出现
bool find() //检查新结点是否目标结点
void outs(); //输出结点状态
void printpath(); //输出路径
void bfs(){ //BFS算法主程序
tnode temp; //tnode型临时结点
int head=0,tail=0; //队列头指针和尾指针
while(head<=tail && tail //根据具体问题确定一个结点扩展规则
temp=state[head]; //取队列头的结点
if(extend()){ //如果该结点可以扩展则产生一个新结点
if(!repeat()){ //如果新结点未曾在队列中出现过则
tail++; // 将新结点加入队列尾
state[tail] =temp;
state[tail].last=head; //记录父结点标识
if(find()){ // 如果新结点是目标结点
hail++; // 将队列尾结点的父结点指针指向队列尾
state[tail] =tail-1;
printpath(); //输出路径
break; //退出程序
}
}
}
head++; //队列头的结点扩展完后出队,取下一结点扩展
}
}
对于不同的问题,用广度优先搜索法的算法基本上都是一样的。但表示问题状态的结点数据结构、新结点是否目标结点和是否重复结点的判断等方面则有所不同,对具体的问题需要进行具体分析,这些函数要根据具体问题进行编写。
二.广度优先搜索算法的例子
下面来看几个简单的例子:
1.分油问题
一个一斤的瓶子装满油,另有一个七两和一个三两的空瓶,再没有其它工具。只用这三个瓶子怎样精确地把一斤油分成两个半斤油。
选择广度优先算法来求解分油问题可以得到通过最少步骤完成分油的最优解。
1)定义状态结点
分油过程实际上就是将油从一个油瓶倒入另一个油瓶。分油过程中,各个油瓶中的油在不断变化,因此需要记录各个油瓶在不同状态所装油的多少。这里用一个数组bottle[3]存放当前油瓶中所装油的多少,不同油瓶用数组下标区分,数组元素bottle[0]是一斤油瓶中的油,bottle[1]是七辆油瓶中的油,而bottle[2]是三两油瓶中的油。
此外,结点中用变量last还要记录每个状态是从哪一个状态变化来的,就是扩展出该结点的父结点编号。
2)扩展规则
很明显,油瓶中必须有油才能把油倒出,同样油瓶必须不满才能将油倒入。分油过程中,将油从一个油瓶倒入另一个油瓶,可能的情形用变量i表示,一共只有6种,每种情形的序号与油瓶编号的关系如下表所示:
分油情形 i 0 1 2 3 4 5
倒出油的油瓶 i/2 0 0 1 1 2 2
倒入油的油瓶 (i+3)/2 Mod 3 1 1 2 0 0 1
3)重复结点和目标结点的判断
结点是否相同只需比较油瓶的状态。对于重复结点,需要将队列中的结点逐一检查,目标结点的判断则比较简单。
4)程序代码如下(VC6.0下编译通过):
#include
#include
const maxn=100;
struct tnode{
int bottle[3]; //当前油瓶装的油
int last; //父结点
int souc; //源瓶
int dest; //目标瓶
}state[maxn]; //状态队列
int capacity[3]; //油瓶容量
void init(){ //初始化
state[0].bottle[0]=10;
state[0].bottle[1]=0;
state[0].bottle[2]=0;
state[0].last=0;
state[0].souc=0;
state[0].dest=0;
capacity[0]=10;
capacity[1]=7;
capacity[2]=3;
}
bool expand(tnode& temp,int i,int j){ //扩展结点
if(temp.bottle[i]>0 && capacity[j]>temp.bottle[j]){ //如果源瓶中有油且目标