非常可乐(九度 OJ 1457)
1.题目描述:
大家一定觉的运动以后喝可乐是一件很惬意的事情,但是 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
2.基本思路
刚看到题,乍一看这不是当年数模校赛的分酒问题吗?!又想起了当年在虹桥机场的那场泊松分酒盛宴。记得当初的问题问的是如何利用最少的步骤来平分酒,我们首先将所有的状态用图存了起来,然后用Dijkstra算法求得单源最短路径。当初可能主要是用了一大堆面向对象的方法来定义节点类,边类等等,导致问题有点繁杂,现在回过头来看还是可以用较短的代码就可以解决的。
扯当年,扯得有点远了,好吧,言归正传。
首先这是一个搜索的问题。这里我们采用BFS的方式来进行搜索,每一个状态下面最多有六种状态,即可以扩展六个结点,即(S->M,S->N,M->N,M->S,N->M,N->S这六种情况)。每次扩展之前需要先检查该状态是否可以扩展,或该状态是否已经被访问过了,以此来进行适当地剪枝。求解的过程有一些细节需要注意:首先就是引用传递,可以节省变量的创建,使得代码更加简洁。还有就是在多组测试用例的情况下,没运行一组之前都要把有关记录状态信息的存储结构恢复初始状态。
还有这个题目其实有些地方没讲清楚:
- ①可乐被平分是指要完全分完吗,还是只要保证两个人分到的是一样的就可以了,不用管是否还有多余的。这个根据例题所给的代码来看是S必须被平分为两个 S 2 \frac{S}{2} 2S才可以的,那么S,M,N为整数那么可以断定知道S%2!=0就一定不能被平分。
3.代码实现
#include <iostream>
#include <queue>
#define NUM 100
using namespace std;
struct Node{
int s,n,m;//三个杯子中可乐的体积
int t;//到达该状态耗费的步数
};
queue<Node> Q;//存储状态结点
bool mark[NUM][NUM][NUM]={false};//用于表示某一种状态是否已经被搜素过了
int S,N,M;//三个杯子的容积
bool _find= false;
void pour(int a,int& ac,int b,int& bc){//倒可乐a->b,ac为杯子a中当前可乐的体积,bc为杯子b中当前可乐的体积
if((b-bc)>ac){//a可以全倒过去
bc = bc + ac;
ac = 0;
}
else{//a只能倒一部分
ac = ac-(b-bc);//注意,两句话的顺序不能反,找了好久的Bug。。
bc = b;
}
}
int main()
{
while(scanf("%d%d%d",&S,&N,&M)!=EOF){
if(S==0&&N==0&&M==0)break;
//记得每次开始前要清空上一个case遗留的信息
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;
}
}
}
while(Q.empty()==false){//初始时先清空队列
Q.pop();
}
_find = false;//标记是否有解
Node n;
n.s = S;
n.m = 0;
n.n = 0;
n.t = 0;
mark[S][0][0]=true;
Q.push(n);
while(Q.empty()==false){
Node node = Q.front();//取队首结点
Q.pop();
printf("s=%d,n=%d,m=%d,t=%d\n",node.s,node.n,node.m,node.t);
if(node.m==S/2&&node.n==S/2||node.m==S/2&&node.s==S/2||node.n==S/2&&node.s==S/2){//判断是否达到终止条件
_find = true;
printf("%d\n",node.t);
break;
}//实际上该过程可以在倒完就进行判断,不要再加入到队列中,等取出来的时候再进行判断。这里为了精简代码,做此处理
//倒酒的过程,有6种情况
//case1:M->N
// printf("node address:%d\n",&node);
Node node1;
node1= node;
// printf("node1 address:%d\n",&node1);
if(node.m>0){
pour(M,node1.m,N,node1.n);
if(mark[node1.s][node1.n][node1.m]==false){
node1.t++;
Q.push(node1);
mark[node1.s][node1.n][node1.m]=true;
}
}
//case2:M->S
Node node2;
node2= node;
if(node.m>0){
pour(M,node2.m,S,node2.s);
if(mark[node2.s][node2.n][node2.m]==false){
node2.t++;
Q.push(node2);
mark[node2.s][node2.n][node2.m]=true;
}
}
//case3:N->M
Node node3;
node3= node;
if(node.n>0){
pour(N,node3.n,M,node3.m);
if(mark[node3.s][node3.n][node3.m]==false){
node3.t++;
Q.push(node3);
mark[node3.s][node3.n][node3.m]=true;
}
}
//case4:N->S
Node node4;
node4= node;
if(node.n>0){
pour(N,node4.n,S,node4.s);
if(mark[node4.s][node4.n][node4.m]==false){
node4.t++;
Q.push(node4);
mark[node4.s][node4.n][node4.m]=true;
}
}
//case5:S->M
Node node5;
node5= node;
if(node.s>0){
pour(S,node5.s,M,node5.m);
if(mark[node5.s][node5.n][node5.m]==false){
node5.t++;
Q.push(node5);
mark[node5.s][node5.n][node5.m]=true;
}
}
//case6:S->N
Node node6;
node6= node;
if(node.s>0){
pour(S,node6.s,N,node6.n);
if(mark[node6.s][node6.n][node6.m]==false){
node6.t++;
Q.push(node6);
mark[node6.s][node6.n][node6.m]=true;
}
}
}
if(_find==false)
printf("NO\n");//
}
return 0;
}
/*
7 4 3
4 1 3
0 0 0
*/
以下代码为讨论区里面有人共享的,利用数学运算的规律求解该问题,可读性比较差:
#include <cstdio>
int gcd(int x, int y)
{
return y ? gcd(y, x % y) : x;
}
int main()
{
int a, b, c;
while (scanf("%i%i%i", &a, &b, &c), a + b + c) {
(a /= gcd(b, c)) & 1 && ~puts("NO") || printf("%i\n", a - 1);
}
return 0;
}