原题叙述
题目描述
倒水问题 “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列开始,不应该有空行或任何尾随空格。
输入样例
2 7 5
2 7 4
输出样例
fill B
pour B A
success
fill A
pour A B
fill A
pour A B
success
题目重述
题目的字面意义比较简单就是给两个容量互质的杯子,一个是a,一个是b,使用这两个杯子,倒出一杯c容量的水,且一直该问题是有解的。
解题思路
思路概述
该题目题意看起来非常简单易懂,但第一次做可能先想到的并非使用BFS而是贪心,但经过尝试两种做法后,BFS的解法相对容易实现,思考边界条件的难度较小,适合解题使用。因此该处叙述BFS解法:
隐视图的核心思想就是将问题转化为BFS可以进行搜索的类型,并且找到各个状态之间转换关系和边界条件。
题意转换:将倒水的过程中两个杯子的状态视为同一个struct,就可以将两个杯子的问题转换为多个状态的问题,题目的求解也可以转换为从初始状态进行BFS搜索得到最终状态(A or B=c)
状态间的转换关系:
(这里我们用a,b,c代表杯子容量,A,B代表某个时间杯子的状态)
状态 | 采取的转换行为 |
---|---|
A>0 | 倒空A |
B>0 | 倒空B |
A<a | 倒满A |
B<b | 倒满B |
A< a&& B>0 && A+B<a | B倒向A,且倒不满 |
A<a && B>0 && A+B>=a | B倒向A,将A倒满 |
B<b && A>0 && A+B<b | A倒向B,且倒不满 |
B<b && A>0 && A+B>=b | A倒向B,将B倒满 |
BFS的边界条件:
A=c || B=c
数据存储
这个问题中,最重要的数据莫过于在倒水过程中,两个状态之间的转换,为了保证在BFS过程,每两个状态之间的转换只出现一次,从而保证找到的倒水次数一定最少,我们使用map<status,status>,来存储两个状态之间的转换行为,在BFS到一个状态后,我们只需要对这个转换行为做这样的操作:
void refresh(status &s, status &t)
{
if ( from.find(t) == from.end() )
{ // 特判合法,加入队列
from[t] = s;
Q.push(t);
}
}
就可以保证加入队列的状态是一个新的状态,即这个转换是合法的
因为在每一次进行BFS转换到一个状态是都要进行一个合法性的判断,我建议将这段代码封装成一个函数,会大大地增强代码的可读性。
map用不熟的小伙伴请移步:c++官方文档
总结
一道使用BFS蛮有意思的题目,在找到方法之后就能够轻松地打出代码,码力要求不高,但是需要你有思想。掌握BFS的核心也就是状态的转移和边界条件,就能将复杂的问题转换为简单的BFS板子。
改进点
无论BFS还是DFS说到底就是一种枚举,要遍历各种情况,但是贪心或者更好的算法能够减少很多访问点,节省时间,所以可能有比这种算法耗时更少的算法,但是从比赛解题的角度看,BFS的易于实现度仍然是在面对低时耗要求题目时比较好的一种实现方法。
问题源码(c++)
#include<cstring>
#include<cstdio>
#include<iostream>
#include<map>
#include<queue>
using namespace std;
struct status
{
/* data */
int a,b;
bool operator<(const status &s)const{
if(a!=s.a) return a<s.a;
return b<s.b;
}
bool operator==(const status &s)const{
return a==s.a && b==s.b;
}
};
queue<status> Q;
map<status, status> from;
vector<status> ans;
/* 递归输出方案 */
void print(status &p,int A,int B,int C)
{
ans.push_back(p);
while( from.find(p) != from.end() && (p.a!=0 | p.b!=0))
{
p=from[p];
ans.push_back(p);
}
for(int i=ans.size()-1;i>0;i--)
{
status start=ans[i];
status end=ans[i-1];
if(start.b==end.b)
{
if(start.a<A && end.a==A)
{
printf("fill A\n");
}
else if(start.a>0 && end.a==0)
{
printf("empty A\n");
}
}
else if(start.a==end.a)
{
if(start.b<B && end.b==B)
{
printf("fill B\n");
}
else if(start.b>0 && end.b==0)
{
printf("empty B\n");
}
}
else if(start.a<end.a && start.b>end.b)
{
printf("pour B A\n");
}
else if(start.a>end.a && start.b<end.b)
{
printf("pour A B\n");
}
}
printf("success\n");
}
void refresh(status &s, status &t)
{
if ( from.find(t) == from.end() )
{ // 特判合法,加入队列
from[t] = s;
Q.push(t);
}
}
void bfs(int A, int B, int C)
{
// 起点, 两杯水都空
status s,t; s.a=0; s.b=0;
Q.push(s);
while (!Q.empty())
{
// 取队首
s = Q.front(); Q.pop();
// 特判到达终点
if (s.a == C || s.b == C) {
print(s,A,B,C); // 输出方案
while(!Q.empty())
Q.pop();
from.clear();
ans.clear();
return;
}
// 倒空 a 杯的水
if (s.a > 0) {
t.a = 0; // 倒空
t.b = s.b;// b 杯不变
refresh(s, t);
}
// 同理,倒空 b 杯的水
if (s.b > 0) {
t.b = 0; // 倒空
t.a = s.a;// a 杯不变
refresh(s, t);
}
// a 杯未满,续满 a 杯
if (s.a < A)
{
// 续满 a 杯
t.a = A;
t.b = s.b;
refresh(s, t);
}
// 同理,b 杯未满,续满 b 杯
if (s.b < B)
{
//续满 b 杯
t.a = s.a;
t.b = B;
refresh(s, t);
}
if (s.b != 0 && s.a<A)
{
if (s.a + s.b <= A)
{
t.a = s.a + s.b;
t.b = 0;
refresh(s, t);
} else
{
t.a = A;
t.b = s.a + s.b - A;
refresh(s, t);
}
}
if (s.a != 0 && s.b<B)
{
if (s.a + s.b <= B)
{
t.a = 0;
t.b = s.a + s.b;
refresh(s, t);
} else
{
t.a = s.a + s.b - B;
t.b = B;
refresh(s, t);
}
}
}
printf("-1\n");
}
int main()
{
int a, b, c;
while(cin>>a>>b>>c)
{
bfs(a, b, c);
}
return 0;
}/* 26 29 11 */