B-Pour Water
一、问题描述
倒水问题 “fill A” 表示倒满A杯,"empty A"表示倒空A杯,“pour A B” 表示把A的水倒到B杯并且把B杯倒满或A倒空。
Input
输入包含多组数据。每组数据输入 A, B, C 数据范围 0 < A <= B 、C <= B <=1000 、A和B互质。
Output
你的程序的输出将由一系列的指令组成。这些输出行将导致任何一个罐子正好包含C单位的水。每组数据的最后一行输出应该是“success”。输出行从第1列开始,不应该有空行或任何尾随空格。
Sample Input
2 7 5
2 7 4
Sample Output
fill B
pour B A
success
fill A
pour A B
fill A
pour A B
success
Notes
如果你的输出与Sample Output不同,那没关系。对于某个"A B C"本题的答案是多解的,不能通过标准的文本对比来判定你程序的正确与否。 所以本题由 SPJ(Special Judge)程序来判定你写的代码是否正确。
二、思路与算法
首先,应该由题意辨别出这个问题是隐式图问题,可以使用BFS方法解决。
隐式图问题:仅给出初始节点、目标节点、生成子节点的约束条件(由题意隐含给出)。
本题中初始节点为A、B杯中都为0(杯空),目标节点为A、B中任意一方水量为C,约束条件为A、B中水量不能超过自身容量等。
编程思路为:输入并存储——>BFS搜索——>输出进行的所有操作。
输入并存储很简单,可用int型直接存储。
BFS搜索需要做抽象概念的转变,我们每次操作有6种可能:把A倒入B(AtoB)、把B倒入A(BtoA)、倒空A(emptyA)、倒空B(emptyB)、倒满A(fillA)、倒满B(fillB)。这六个动作类似于迷宫问题中一个点的周围四个点,它们分别通向6个相邻状态。
所以,我们利用队列queue,每次都check这六种动作,看是否可以达成目标节点。
输出进行的所有操作,类比于输出最短路径,我们也需要声明一些新变量,用于存储有关操作顺序的数据。
本次输出数据用string类存储,用vector数组存储所有要输出的内容,这样和用栈/队列等数据结构存储大同小异,选用顺手的存储即可。
至此,分析完成,按此思路编写的代码如下。(更详细的解释在注释中体现)
三、代码实现
#include<iostream>
#include<vector>
#include<map>
#include<queue>
#include<cstdio>
#include<string>
using namespace std;
struct status{
int x,y;
string action; //记录为了达到这个状态,上一个状态进行了什么操作
status(){} //重载构造函数
status(int xx,int yy){ x=xx; y=yy; }
bool operator < (const status &s)const{
if(x!=s.x){ return x<s.x; }
else{ return y<s.y; }
} //重载操作符(大小比较方法)
bool operator == (const status &s)const{
return (x==s.x)&&(y==s.y); //重载=
}
//六个函数,分别代表了六种操作
status AtoB(int B){
status c;
c.x=max(x+y-B,0);
c.y=min(B,x+y);
return c;
}
status BtoA(int A){
status c;
c.y=max(x+y-A,0);
c.x=min(A,x+y);
return c;
}
status fillA(int A){ return status(A,y); }
status fillB(int B){ return status(x,B); }
status emptyA(){ return status(0,y); }
status emptyB(){ return status(x,0); }
};
vector<status> ans; //用于输出操作,按序存放采用的操作
map<status,bool> mp; //存储某状态是否已经出现过
map<status,status> from; //存储状态的变化过程,前面由后面操作得来
queue<status> q; //队列,用于BFS操作
void check(status x,status y,int acNum){
if(mp[x]==0){
mp[x]=1; //打标记
from[x]=y;
switch (acNum){
case 1:{x.action="pour A B"; break;}
case 2:{x.action="pour B A"; break;}
case 3:{x.action="fill A"; break;}
case 4:{x.action="fill B"; break;}
case 5:{x.action="empty A"; break;}
case 6:{x.action="empty B"; break;}
}
q.push(x); //入队列
}
}
//输出操作函数
void print(status s){
ans.push_back(s); //加入数组中
while(from.find(s)!=from.end()){ //未找到时,不断放入数组
s=from[s];
ans.push_back(s);
}
for(int i=ans.size()-2;i>=0;i--){ //按照数组顺序,输出操作
printf("%s\n",ans[i].action.c_str());
}
printf("success\n");
}
void bfs(int a,int b,int A,int B,int C){
status now(a,b); //初始状态
q.push(now); //入队列
mp[now]=1; //打上标记
while(!q.empty()){
now=q.front();
q.pop();
if(now.x==C||now.y==C){
print(now);
return;
}//成功,返回
check(now.AtoB(B),now,1);
check(now.BtoA(A),now,2);
check(now.fillA(A),now,3);
check(now.fillB(B),now,4);
check(now.emptyA(),now,5);
check(now.emptyB(),now,6);
}
}
int main(){
int A,B,C;
while(cin>>A>>B>>C){
mp.clear();
from.clear();
ans.clear();
while(!q.empty()){
q.pop();
} //清空数据结构
bfs(0,0,A,B,C);
}
return 0;
}
四、经验与总结
- 切记! 要输出string类时,不能直接printf("%s",action),必须是action.c_str()。
- map尽量不要直接在声明时就赋初值,因为只有c++11及以后版本才支持这种操作。
- 提交时出现了TEL问题,检查后发现,错在多次输入时,没有在一次输出之后清空数据结构,导致后面数据结构中内容越来越多,遍历时用时过多。
所以,不要忘记每次操作结束后调用clear()进行清空! - 更换要输出的内容,可能也会引起很大的改变,因为要更改很多数据结构或者声明新的数据结构来存储要输出的数据,而不仅仅是更改cout那么简单。