非常可乐 - 九度 OJ 1457
题目
时间限制:1 秒 内存限制:32 兆 特殊判题:否
题目描述:
大家一定觉得运动以后喝可乐是一件很惬意的事情,但是 seeyou 却不这么认为。因为每次当 seeyou 买了可乐以后,阿牛就要求和 seeyou 一起分享这一瓶可乐,而且一定要喝的和 seeyou 一样多。但 seeyou 的手中只有两个杯子,它们的容量分别是 N 毫升和 M 毫升。可乐的体积为 S (S<101)毫升(正好装满一瓶) ,它们三个之间可以相互倒可乐 (都是没有刻度的,且 S==N+M,101>S>0,N>0,M>0) 。聪明的 ACMER 你们说他们能平分吗?如果能请输出倒可乐的最少的次数,如果不能输出"NO"。
输入:
三个整数 : S 可乐的体积 , N 和 M 是两个杯子的容量,以"0 0 0"结束。
输出:
如果能平分的话请输出最少要倒的次数,否则输出"NO"。
样例输入:
7 4 3
4 1 3
0 0 0
样例输出:
NO
3
这是一个非常能够说明状态搜索含义的题。题面中丝毫没有涉及到图的概念,也没有给出任何图模型。那么,它也能进行搜索么?答案是肯定的,搜索的途径即是对状态进行搜索。
使用四元组(x,y,z,t)来表示一个状态,其中 x、y、z 分别表示三个瓶子中的可乐体积,t 表示从初始状态到该状态所需的杯子间互相倾倒的次数。状态间的相互扩展,就是任意四元组经过瓶子间的相互倾倒而得到若干组新的四元组的过程。这样,当平分的状态第一次被搜索出来以后,其状态中表示的杯子倾倒次数即是所求。同样的,由于要搜索的是最少倒杯子次数,若四元组(x,y,z,t)中 t 并不是得到体积组 x、y、z 的最少倒杯子次数,那么该状态为无效状态,将其舍弃。
#include <stdio.h>
#include <queue>
using namespace std;
struct N{
//状态结构体
int a,b,c;//每个杯子中可乐的体积
int t;//得到该体积组倾倒次数
};
queue<N> Q;//队列
bool mark[101][101][101];
//对体积组(x,y,z)进行标记,即只有第一次得到包含
//体积组(x,y,z)的状态为有效状态,其余的舍去
void AtoB(int &a,int sa,int &b,int sb){
//倾倒函数,由容积为sa的杯子倒往容积为sb的杯子,
//其中引用参数a和b,初始时为原始杯子中可乐的体积,
//当函数调用完毕后,为各自杯中可乐的新体积
if(sb-b >= a){
//若a可以全部,即倒入a体积可乐至b中
b+=a;
a=0;
}else{//否则,a可以倒入(sb-b)体积可乐至b中
a-=sb-b;
b=sb;
}
}
int BFS(int s,int n,int m){
while(Q.empty()==false){
//当队列非空时,重复循环
N now=Q.front();//拿出队头状态
Q.pop();//弹出队头状态
int a,b,c;//a,b,c临时保存三个杯子中可乐体积
a=now.a;
b=now.b;
c=now.c;//读出该状态三个杯子中可乐体积
AtoB(a,s,b,n);//由a倾倒向b
if(mark[a][b][c]==false){
//若该体积组尚未出现
mark[a][b][c]=true;//标记该体积组
N tmp;
tmp.a=a;
tmp.b=b;
tmp.c=c;
tmp.t=now.t+1;//生成新的状态
if(a==s/2 && b==s/2)return tmp.t;
if(c==s/2 && b==s/2)return tmp.t;
if(a==s/2 && c==s/2)return tmp.t;
//若该状态已经为平分状态,则直接返回该状态的耗时
Q.push(tmp);//否则放入队列
}
a=now.a;
b=now.b;
c=now.c;//重置a,b,c为未倾倒前的体积
AtoB(b,n,a,s);//由b倾倒向a
if(mark[a][b][c]==false){
mark[a][b][c]=true;
N tmp;
tmp.a=a;
tmp.b=b;
tmp.c=c;
tmp.t=now.t+1;
if(a==s/2 && b==s/2)return tmp.t;
if(c==s/2 && b==s/2)return tmp.t;
if(a==s/2 && c==s/2)return tmp.t;
Q.push(tmp);
}
a=now.a;
b=now.b;
c=now.c;
AtoB(a,s,c,m);//由a倾倒向c
if(mark[a][b][c]==false){
mark[a][b][c]=true;
N tmp;
tmp.a=a;
tmp.b=b;
tmp.c=c;
tmp.t=now.t+1;
if(a==s/2 && b==s/2)return tmp.t;
if(c==s/2 && b==s/2)return tmp.t;
if(a==s/2 && c==s/2)return tmp.t;
Q.push(tmp);
}
a=now.a;
b=now.b;
c=now.c;
AtoB(c,m,a,s);//由c倾倒向a
if(mark[a][b][c]==false){
mark[a][b][c]=true;
N tmp;
tmp.a=a;
tmp.b=b;
tmp.c=c;
tmp.t=now.t+1;
if(a==s/2 && b==s/2)return tmp.t;
if(c==s/2 && b==s/2)return tmp.t;
if(a==s/2 && c==s/2)return tmp.t;
Q.push(tmp);
}
a=now.a;
b=now.b;
c=now.c;
AtoB(b,n,c,m);//由b倾倒向c
if(mark[a][b][c]==false){
mark[a][b][c]=true;
N tmp;
tmp.a=a;
tmp.b=b;
tmp.c=c;
tmp.t=now.t+1;
if(a==s/2 && b==s/2)return tmp.t;
if(c==s/2 && b==s/2)return tmp.t;
if(a==s/2 && c==s/2)return tmp.t;
Q.push(tmp);
}
a=now.a;
b=now.b;
c=now.c;
AtoB(c,m,b,n);//由c倾倒向b
if(mark[a][b][c]==false){
mark[a][b][c]=true;
N tmp;
tmp.a=a;
tmp.b=b;
tmp.c=c;
tmp.t=now.t+1;
if(a==s/2 && b==s/2)return tmp.t;
if(c==s/2 && b==s/2)return tmp.t;
if(a==s/2 && c==s/2)return tmp.t;
Q.push(tmp);
}
}
return -1;
}
int main()
{
int s,n,m;
while(scanf("%d%d%d",&s,&n,&m)!=EOF){
if(s==0)break;//若s为0,则n,m为0则退出
if(s%2 == 1){
//若s为奇数则不可能平分,直接输出NO
puts("NO");
continue;
}
for(int i=0;i<=s;i++){
for(int j=0;j<=n;j++){
for(int k=0;k<=m;k++){
mark[i][j][k]=false;
}
}
}//初始化状态
N tmp;
tmp.a=s;
tmp.b=0;
tmp.c=0;
tmp.t=0;//初始时状态
while(Q.empty()==false)Q.pop();//清空队列中状态
Q.push(tmp);//将初始状态放入队列
mark[s][0][0]=true;//标记初始状态
int rec=BFS(s,n,m);//广度优先搜索
if(rec==-1){
puts("NO");
}else{
printf("%d\n",rec);//否则输出答案
}
}
return 0;
}
可见,与动态规划问题一样,广度优先搜索的关键也是确定状态。只有确定了需要搜索的状态,才能更好的进行搜索活动。同时,广度优先搜索的复杂度也与状态的数量有关。
由于舍弃了很多无效的状态,那么其时间复杂度与有效状态正相关。如本题所有可能出现的状态为 100100100 个,即每个体积组对应一个有效状态,所以其复杂度也大致为这个数量级,在进行广搜之前要判断其复杂度是否符合要求。
最后,总结广度优先搜索的几个关键字:
1.状态。确定求解问题中的状态。通过状态的转移扩展,查找遍历所有的状态,从而从中寻找需要的答案。
2.状态扩展方式。在广度优先搜索中,总是尽可能扩展状态,并先扩展得出的状态先进行下一次扩展。在解答树上的变现为按层次遍历所有状态。
3.有效状态。对有些状态并不对其进行再一次扩展,而是直接舍弃它。因为根据问题分析可知,目标状态不会由这些状态经过若干次扩展得到。即目标状态,不可能存在其在解答树上的子树上,所以直接舍弃。
4.队列。为了实现先得出的状态先进行扩展,使用队列,将得到的状态依次放入队尾,每次取队头元素进行扩展。
5.标记。为了判断哪些状态是有效的,哪些是无效的往往使用标记。
6.有效状态数。问题中的有效状态数与算法的时间复杂度同数量级,所以在进行搜索之前必须估算其是否在所可以接受的范围内。
7.最优。广度优先搜索常被用来解决最优值问题,因为其搜索到的状态总是按照某个关键字递增(如前例中的时间和倒杯子次数),这个特性非常适合求解最优值问题。所以一旦问题中出现最少、最短、最优等关键字,就要考虑是否是广度优先搜索。