您的位置:首页 > 其它

【算法分析与设计】广度优先搜索

2012-02-22 21:12 344 查看
图论的广度优先搜索经常用于对解空间的搜索,尤其是求某个解,且这个解具有最短步骤的时候,广度优先搜索是极佳的选择。

1.首先应该注意的是解空间的组织,搜索算法的解通常被安排成多个步骤,每个步骤一条边,而从起点到终点的一条路径就构成了一个解。

2.解空间树的存储问题,由于解空间的树,往往并不是二叉树,所以左右孩子的组织方式是不太适合的,所以一般采用记录父结点的方式,那么从终点到起点就一定可以找到一条路径,打印路径一般采用递归打印。这种方式正好也适合广度优先算法一般只找寻某个解的特点。

3.如果每个结点都按所有可能进行扩展,那么最后的解空间无疑是指数级的,这个时候剪枝就显得尤为重要,对于不可能的结点一定要确保剪枝动作的执行,这样才能得到一个相对可行的搜索算法。

算法的框架如下:

begin

建立队列Q

初始状态入队

while(Q非空)

取出队首结点e

if ( e是可行解状态 )

返回e作为结果

if( e不可能是解的某个状态)

剪枝动作,转到取下一个队首结点

for 0->k(所有的可能状态)

生成新的状态结点插入队尾

end

下面以POJ1606:jugs为例说明广度优先搜索的应用:

时间限制:
1000ms
内存限制:
65536kB

描述
In the movie "Die Hard 3", Bruce Willis and Samuel L. Jackson were confronted with the following puzzle. They were given a 3-gallon jug and a 5-gallon jug and were asked to fill the 5-gallon jug with exactly 4 gallons. This problem generalizes that puzzle.

You have two jugs, A and B, and an infinite supply of water. There are three types of actions that you can use: (1) you can fill a jug, (2) you can empty a jug, and (3) you can pour from one jug to the other. Pouring from one jug to the other stops when the
first jug is empty or the second jug is full, whichever comes first. For example, if A has 5 gallons and B has 6 gallons and a capacity of 8, then pouring from A to B leaves B full and 3 gallons in A.

A problem is given by a triple (Ca,Cb,N), where Ca and Cb are the capacities of the jugs A and B, respectively, and N is the goal. A solution is a sequence of steps that leaves exactly N gallons in jug B. The possible steps are

fill A

fill B

empty A

empty B

pour A B

pour B A

success

where "pour A B" means "pour the contents of jug A into jug B", and "success" means that the goal has been accomplished.

You may assume that the input you are given does have a solution.
输入
Input to your program consists of a series of input lines each defining one puzzle. Input for each puzzle is a single line of three positive integers: Ca, Cb, and N. Ca and Cb are the capacities of jugs A and B, and N is the goal. You can assume 0 < Ca <= Cb
and N <= Cb <=1000 and that A and B are relatively prime to one another.
输出
Output from your program will consist of a series of instructions from the list of the potential output lines which will result in either of the jugs containing exactly N gallons of water. The last line of output for each puzzle should be the line "success".
Output lines start in column 1 and there should be no empty lines nor any trailing spaces.
样例输入
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()

算法的更宏观思想是:

初始结点入队

队列非空就做下列动作

取出队首状态结点

如果已经是解,返回

如果需要剪枝,剪枝

如果可以扩展,扩展并入队
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: