图论的广度优先搜索经常用于对解空间的搜索,尤其是求某个解,且这个解具有最短步骤的时候,广度优先搜索是极佳的选择。
1.首先应该注意的是解空间的组织,搜索算法的解通常被安排成多个步骤,每个步骤一条边,而从起点到终点的一条路径就构成了一个解。
2.解空间树的存储问题,由于解空间的树,往往并不是二叉树,所以左右孩子的组织方式是不太适合的,所以一般采用记录父结点的方式,那么从终点到起点就一定可以找到一条路径,打印路径一般采用递归打印。这种方式正好也适合广度优先算法一般只找寻某个解的特点。
3.如果每个结点都按所有可能进行扩展,那么最后的解空间无疑是指数级的,这个时候剪枝就显得尤为重要,对于不可能的结点一定要确保剪枝动作的执行,这样才能得到一个相对可行的搜索算法。
算法的框架如下:
begin
建立队列Q
初始状态入队
while(Q非空)
取出队首结点e
if ( e是可行解状态 )
返回e作为结果
if( e不可能是解的某个状态)
剪枝动作,转到取下一个队首结点
for 0->k(所有的可能状态)
生成新的状态结点插入队尾
end
下面以POJ1606:jugs为例说明广度优先搜索的应用:
3 5 4
5 7 3
fill B
pour B A
empty A
pour B A
fill B
pour B A
success
fill A
pour A B
fill A
pour A B
empty B
pour A B
success
/*全局变量*/ int ca = 0; int cb = 0; int N = 0; /******************************************* 这不是一种好的程序设计方式,但在单个解题时, 有时参数直接在全局,可以减少很多参数传递 *******************************************/ /*状态结点,使用BFS通常需要根据情况设立状态结点*/ struct state { int a; int b; int act; //上一步通过什么操作到达这一步的 struct state* pre;//上一个状态的位置,也可以认为是记录父结点 }; /*********************************************** 这种父节点的记录方式,可以简化许多不必要的存储 但剪枝运算和打印路径的运算会略增加复杂度, 但由于剪枝元算检查路径的时候最多遍历一条路径 而解空间树的高度通常是不可能太高的(32层时,解空间已经是n^32级), 所以这种带检查的剪枝运算不会成为算法的瓶颈 而打印路径只遍历一次路径,更加不可能成为算法的瓶颈 当然也可以将state* pre改为一个数组或者向量 *************************************************/ /************************************************* 这道题目中每步可能采取的动作,其实只是为存储动作的 时候不必要存储字符串,只需要存储一个编号即可 同时也方便打印路径的时候用 **************************************************/ char action[6][10]= { "fill A", "fill B", "empty A", "empty B", "pour A B", "pour B A" }; /************************************* 可以理解为新状态结点的生成动作, 视题目不同而繁简不同 **************************************/ void doAction(state *src,state *dest,int act) { dest->pre = src; dest->act = act; switch(act) { case 0:dest->a = ca; dest->b = src->b;break; case 1:dest->a = src->a; dest->b = cb; break; case 2:dest->a = 0; dest->b = src->b;break; case 3:dest->a = src->a; dest->b = 0; break; case 4:if( cb-src->b >= src->a ){dest->a = 0; dest->b = src->a + src->b;} else {dest->a = src->a -(cb-src->b); dest->b = cb;} break; case 5:if( ca-src->a >= src->b ){dest->a = src->a+src->b; dest->b=0;} else {dest->b = src->b-(ca-src->a); dest->a = ca;} break; } //printf("%d %d %s %d %d\n",src->a,src->b,action[act],dest->a,dest->b); } /******************************** 这道题目的剪枝动作所需要的检查操作 *********************************/ bool check(state *st)//检查st前面的状态是否出现过和st一样的 { state *tmp = st->pre; while(tmp) { if(tmp->a == st->a && tmp->b==st->b) return true; else tmp = tmp->pre; } return false; } /*递归的路径打印函数*/ void print_path(state* st) { if(st->pre ==NULL) return; print_path(st->pre); printf("%s\n",action[st->act]); } int count = 0;//测试统计用的,用来比较剪枝前后搜索动作的数量 /*核心的广度优先搜索函数*/ void BFS(state* &root,int target) { std::deque<state*> state_que; state_que.push_back(root); while(!state_que.empty()) { state *front = state_que.front(); state_que.pop_front(); //printf("%d %d\n",front->a,front->b); if(front->b == target) { root = front; return;} if(check(front)) continue; for(int i=0; i<6; ++i) if(front->act != i) { state* next = new state; doAction(front,next,i); state_que.push_back(next); count++; } } } int main() { while(scanf("%d%d%d",&ca,&cb,&N)!=EOF) { state *root=new state; root->a = 0; root->b = 0; root->act = -1; root->pre = NULL; BFS(root,N); print_path(root); printf("success\n"); } return 0; }
综合来看,设计一个广度优先搜索算法的步骤是:
1.定义状态结点。通常以记录父节点来构造解空间树。
2.确定状态是否是所求解的函数,isAnswer() 太直接的时候可直接写,省略函数
3.确定状态是否需要剪枝的函数,isUnwanted() 通常需要仔细考虑什么情况需要剪枝
4.生成新状态结点的函数,doTransfer() 有时可能比较简单,不需要专门的函数
5.广度优先搜索的核心函数BFS()
算法的更宏观思想是:
初始结点入队
队列非空就做下列动作
取出队首状态结点
如果已经是解,返回
如果需要剪枝,剪枝
如果可以扩展,扩展并入队