ACM中常见广度优先搜索应用之一及注意要点

本文根据一个题目引入:【宽搜入门】巧妙取量

题目描述

有三个容器,容量分别为 a,b,c(a> b > c ),一开始a装满油,现在问是否只靠abc三个容器量出k升油。如果能就输出“yes”,并且说明最少倒几次,否则输出“no”。例如:10升油在10升的容器中,另有两个7升和3升的空容器,要求用这三个容器倒油,使得最后在abc三个容器中有一个刚好存有5升油,问最少的倒油次数是多少?(每次倒油结束条件为一个倒满或者一个倒完(空))

Input

有多组测试数据。
输入a,b,c, k四个正整数( 100 ≥ a > b > c≥1 , 1≤k< 100 )

Output

如果能得到k就输出两行。
第一行“yes”,第二行为最少的次数
否则输出“no”

题目示例分析

10 7 3:容器容量
(10 0 0):初始情况
(3 7 0):第一次,a倒入b中,b倒满,则停止此次倒油
(3 4 3):第二次,b倒入c中,c倒满,则停止此次倒油
(6 4 0):第三次,c倒入a中,c倒完,则停止此次倒油
(6 1 3):第四次,b倒入c中,c倒满,则停止此次倒油
(9 1 0):第五次,c倒入a中,c倒完,则停止此次倒油
(9 0 1):第六次,b倒入c中,b倒完,则停止此次倒油
(2 7 1):第七次,a倒入b中,b倒满,则停止此次倒油
(2 5 3):第八次,b倒入c中,此时出现了5,则到达目标,结束程序。

思路分析

题目的示例之中,其8次的操作是该示例的最优解法,即这样操作的次数是最少的,那么我们如何得到这个操作呢?
通过细读题目我们可以发现,在任何一种状态下,当我们面临该问题时都将有6中操作,分别为a倒入b,a倒入c,b倒入c,b倒入a,c倒入a,c倒入b;而进行了该种操作之后我们又有这6中操作可以选择,直到达到目标容量。由此我们可以思考到,在匹配目标的过程中,我们的操作是一步一步的深入搜寻的,因此可以想成是一个操作接着一个操作,如果最终找到了目标状态,则从开始到结束会形成一条操作序列。在现实生活中,一个人每次都只能选择6种中的一种操作进行,然而此次选择的操作所在的那条序列不一定是最优的,但可能会达到目标。而如果我们每次操作时同时让六个人分别进行六种不同的操作呢?(虽然会需要很多很多的人,^_^)第一次(也可叫第一层)只需要六个人,而第二次每个人又会有六种操作,那么属于第二层的操作共需要36个人,第三层的操作则需要216个人......,依次下去,从这一分析看来,每层的操作都是遍历完了的,这样一层一层下去,只要遇到了目标,那么该条操作序列的总次数一定是最小的。而这也是广度优先搜索的思想。因此我们便可以使用广度优先搜索算法来解决这一问题。

如果直接通过上面的思路编写程序,在一定的范围内我们是可以得到正确结果的,但是对于各大oj上的题目,我们提交就会发现运行超时。(对于这一问题,本人也是在经过现实的坑之后才知道的,一开始也不知道,所以读者不用想如何一开始就能知道这一坑。)通过对问题分析,确实需要搜索的状态比较多,每次操作之后都又会增加六倍的操作次数,然而可能的就是某一次操作过后的一个容量状态是之前已经搜索过的,这个可以自己举例,因此原因程序执行时会访问一些已经访问过的状态,所以我们可以通过判断当前状态是否访问过来决定是否需要继续当前的状态的后续操作。终止当前过程的操作也叫做剪枝,读者可自行查阅资料并理解,不知道对问题也无影响。

因此利用访问控制,便可以正确通过oj的测试了。本问题的代码如下:

#include
#include
#include
#include
#include


using namespace std;
//输入变量定义 
int a, b, c, k;
//访问标记 
bool vis[101][101][101];

//某个状态的节点定义 
struct node{
    int a, b, c;
    int step;
}top, temp;

//利用广度优先算法解决 
int bfs(node status){
    //定义队列 
    queue q;
    //放入初始状态 
    q.push(status);
    //设置已访问 
    vis[status.a][status.b][status.c] = true;
    //访问直到队列为空 
    while(!q.empty()){
        //每次取出队头状态进行后续操作 
        top = q.front();
        q.pop();
        //如果当前的状态已经达到了目标状态则直接退出 
        if(top.a == k || top.b == k || top.c == k)
            return top.step;
        //进行以下的六种操作,注意由于利用了队列及广度的思想故六种操作整体上可理解是并列进行的 
        //a倒入b,前提是需要满足a能倒入b中,即a中有油,b还未装满 
        if(top.a > 0 && top.b < b){
            //选取需要转移的油的容量,比如a中还有3升,b中还可装5升,那么需要转移的就是3升
            //同理可以想象a中剩的比b中可装的多的情况 
            int _min = min(top.a, b-top.b);
            //修改当前状态 
            temp.a = top.a - _min;
            temp.b = top.b + _min;
            temp.c = top.c;
            //将其层数加1,也就相当于将其操作步数加1
            temp.step = top.step + 1;
            //判断当前状态是否访问,未访问时才继续后面的操作 
            if(!vis[temp.a][temp.b][temp.c]){
                //加入队列并设置已访问 
                q.push(temp);
                vis[temp.a][temp.b][temp.c] = true;
            }
        }
        //下面的操作则和a倒入b中的操作相同 
        //a倒入c 
        if(top.a > 0 && top.c < c){
            int _min = min(top.a, c-top.c);
            temp.a = top.a - _min;
            temp.b = top.b;
            temp.c = top.c + _min;
            temp.step = top.step + 1;
            if(!vis[temp.a][temp.b][temp.c]){
                q.push(temp);
                vis[temp.a][temp.b][temp.c] = true;
            }
        }
        //b倒入c 
        if(top.b > 0 && top.c < c){
            int _min = min(top.b, c-top.c);
            temp.a = top.a;
            temp.b = top.b - _min;
            temp.c = top.c + _min;
            temp.step = top.step + 1;
            if(!vis[temp.a][temp.b][temp.c]){
                q.push(temp);
                vis[temp.a][temp.b][temp.c] = true;
            }
        }
        //b倒入a 
        if(top.b > 0 && top.a < a){
            int _min = min(top.b, a-top.a);
            temp.a = top.a + _min;
            temp.b = top.b - _min;
            temp.c = top.c;
            temp.step = top.step + 1;
            if(!vis[temp.a][temp.b][temp.c]){
                q.push(temp);
                vis[temp.a][temp.b][temp.c] = true;
            }
        }
        //c倒入a 
        if(top.c > 0 && top.a < a){
            int _min = min(top.c, a-top.a);
            temp.a = top.a + _min;
            temp.b = top.b;
            temp.c = top.c - _min;
            temp.step = top.step + 1;
            if(!vis[temp.a][temp.b][temp.c]){
                q.push(temp);
                vis[temp.a][temp.b][temp.c] = true;
            }
        }
        //c倒入b 
        if(top.c > 0 && top.b < b){
            int _min = min(top.c, b-top.b);
            temp.a = top.a;
            temp.b = top.b + _min;
            temp.c = top.c - _min;
            temp.step = top.step + 1;
            if(!vis[temp.a][temp.b][temp.c]){
                q.push(temp);
                vis[temp.a][temp.b][temp.c] = true;
            }
        }
    }
    return -1;
}


int main(){
    
    while(scanf("%d%d%d%d", &a, &b, &c, &k) != EOF){
        memset(vis, 0, sizeof(vis));
        temp.a = a;
        temp.b = 0;
        temp.c = 0;
        temp.step = 0;
        int res = bfs(temp);
        if(res == -1)
            printf("no\n");
        else
            printf("yes\n%d\n", res);
    }
    
    return 0;
}

注意点总结

1.对于当前操作的确定,当前的所有操作都要确定并且都要加入队列,一种操作就会产生一种状态;
2.对于已访问状态的判断,事实证明判断之后可以减少很多的重复操作;
3.用于判断是否访问的数据结构有多种,由于本文涉及的题目可以直接使用三维数组解决所以直接使用了三维数组,另外则还可以使用map实现;
4.对于是否会结束循环的思考,初学者很容易产生一个问题就是这样搜索如果没搜索到真的会结束吗?为什么搜索到的就是最优情况呢?对于第一个问题,如果真的没有对应的目标状态,那么其实程序会例举完所有的状态,而这所有的状态我们是很难一一列举出来的。当例举完后,队列就会为空,因此会退出循环。对于第二个问题,读者可以使用通过查阅迷宫广度找最短路相关的资料进行理解,广度是一层一层向下的,而目标肯定是存在于某一层的一个节点,只要算法能一层一层下去,首先搜索到目标节点之后形成的路径或者序列便是最优的。
5.最后总结一点则是对于适合用广度解决的一类问题,通常题目都会给予相应的几种操作,然后求解最少的或者最优的相关答案。

你可能感兴趣的:(ACM中常见广度优先搜索应用之一及注意要点)