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<cstdio>
#include<iostream>
#include<cmath>
#include<algorithm>
#include<queue>


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<node> 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.最后总结一点则是对于适合用广度解决的一类问题,通常题目都会给予相应的几种操作,然后求解最少的或者最优的相关答案。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,527评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,314评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,535评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,006评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,961评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,220评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,664评论 3 392
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,351评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,481评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,397评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,443评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,123评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,713评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,801评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,010评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,494评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,075评论 2 341

推荐阅读更多精彩内容

  • 专业考题类型管理运行工作负责人一般作业考题内容选项A选项B选项C选项D选项E选项F正确答案 变电单选GYSZ本规程...
    小白兔去钓鱼阅读 8,966评论 0 13
  • 选择题部分 1.(),只有在发生短路事故时或者在负荷电流较大时,变流器中才会有足够的二次电流作为继电保护跳闸之用。...
    skystarwuwei阅读 12,665评论 0 7
  • 选择题部分 1.()部门负责日常监督检查工作,安全巡视的同时进行消防检查,推动消防安全制度的贯彻落实。 A: 消防...
    skystarwuwei阅读 14,903评论 0 3
  • 高级钳工应知鉴定题库(858题) ***单选题*** 1. 000003难易程度:较难知识范围:相关4 01答案:...
    开源时代阅读 5,639评论 1 9
  • 01. 颅脑CT扫描采用的听眶线是()。 (1.0 分) A. 外耳孔与外眼眦的连线 B. 外耳孔上缘与眶下缘的连...
    我们村我最帅阅读 3,117评论 0 6