当前位置: 代码迷 >> 综合 >> Codeup100000609问题 A: Jugs
  详细解决方案

Codeup100000609问题 A: Jugs

热度:82   发布时间:2024-02-06 15:07:59.0

题目描述:

In the movie “Die Hard 3”, Bruce Willis and Samuel L. Jackson were confronted with the following puzzle. They were given a 3-gallon jug and a 5-gallon jug and were asked to fill the 5-gallon jug with exactly 4 gallons. This problem generalizes that puzzle.

You have two jugs, A and B, and an infinite supply of water. There are three types of actions that you can use: (1) you can fill a jug, (2) you can empty a jug, and (3) you can pour from one jug to the other. Pouring from one jug to the other stops when the first jug is empty or the second jug is full, whichever comes first. For example, if A has 5 gallons and B has 6 gallons and a capacity of 8, then pouring from A to B leaves B full and 3 gallons in A.

A problem is given by a triple (Ca,Cb,N), where Ca and Cb are the capacities of the jugs A and B, respectively, and N is the goal. A solution is a sequence of steps that leaves exactly N gallons in jug B. The possible steps are

fill A 
fill B 
empty A 
empty B 
pour A B 
pour B A 
success

where “pour A B” means “pour the contents of jug A into jug B”, and “success” means that the goal has been accomplished.
You may assume that the input you are given does have a solution.

输入:

Input to your program consists of a series of input lines each defining one puzzle. Input for each puzzle is a single line of three positive integers: Ca, Cb, and N. Ca and Cb are the capacities of jugs A and B, and N is the goal. You can assume 0 < Ca <= Cb and N <= Cb <=1000 and that A and B are relatively prime to one another.

输出:

Output from your program will consist of a series of instructions from the list of the potential output lines which will result in either of the jugs containing exactly N gallons of water. The last line of output for each puzzle should be the line “success”. Output lines start in column 1 and there should be no empty lines nor any trailing spaces.

样例输入:

3 7 1
9 32 6

样例输出:

fill B
pour B A
empty A
pour B A
success
fill B
pour B A
empty A
pour B A
empty A
pour B A
empty A
pour B A
fill B
pour B A
empty A
pour B A
empty A
pour B A
empty A
pour B A
empty A
pour B A
fill B
pour B A
empty A
pour B A
empty A
pour B A
success

提示:

倒水问题的经典形式是这样的:

“假设有一个池塘,里面有无穷多的水。现有2个空水壶,容积分别为5升和6升。问题是如何只用这2个水壶从池塘里取得3升的水。”

当然题外是有一些合理的限制的,比如从池塘里灌水的时候,不管壶里是不是已经有水了,壶一定要灌满,不能和另一个壶里的水位比照一下“毛估估”(我们可以假设壶是不透明的,而且形状也不同);同样的,如果要把水从壶里倒进池塘里,一定要都倒光;如果要把水从一个壶里倒进另一个壶里,也要都倒光,除非在倒的过程中另一个壶已经满了;倒水的时候水没有损失(蒸发溢出什么的)等等等等。

事实上,要解决上面这题,你只要用两个壶中的其中一个从池塘里灌水,不断地倒到另一个壶里,当第二个壶满了的时候,把其中的水倒回池塘里,反复几次,就得到答案了。以5升壶(A)灌6升壶(B)为例:

A  B
0  0
5  0  A→B
0  5
5  5  A→B
4  6
4  0  A→B
0  4
5  4  A→B
3  6

现在我们问,如果是多于2只壶的情况怎么办(这样一来就不能用上面的循环倒水法了)?如何在倒水之前就知道靠这些壶是一定能(或一定不能)倒出若干升水来的?试举数例:
1) 两个壶:65升和78升,倒38升和39升。
2) 三个壶:6升,10升和45升,倒31升。

我们可以看到,在1)中,65=5×13,78=6×13,而39=3×13。所以如果把13升水看作一个单位的话(原题中的“升”是没有什么重要意义的,你可以把它换成任何容积单位,毫升,加仑——或者“13升”),这题和最初的题目是一样的。而38升呢?显然是不可能的,它不是13的倍数,而65升和78升的壶怎么也只能倒出13升的倍数来。也可以这样理解:这相当于在原题中要求用5升和6升的壶倒出38/39升来。

那么2)呢?你会发现,只用任何其中两个壶是倒不出31升水的,理由就是上面所说的,(6,10)=2,(6,45)=3,(10,45)=5,(这里(a,b)是a和b的最大公约数),而2,3,5均不整除31。可是用三个壶就可以倒出31升:用10升壶四次,6升壶一次灌45升壶,得到1升水,然后灌满10升壶三次得30升水,加起来为31升。

一般地我们有“灌水定理”:

“如果有n个壶容积分别为A1,A2,……,An(Ai均为大于0的整数)设w为另一大于0的整数。则用此n个壶可倒出w升水的充要条件为:
1) w小于等于A1+A2+…+An;
2) w可被(A1,A2,…,An)(这n个数的最大公约数)整除。”

这两个条件都显然是必要条件,如果1)不被满足的话,你连放这么多水的地方都没有。2)的道理和上面两个壶的情况完全一样,因为在任何步骤中,任何壶中永远只有(A1,A2,…,An)的倍数的水。

现在我们来看一下充分性。在中学里我们学过,如果两个整数a和b互素的话,那么存在两个整数u和v,使得ua+vb=1。证明的方法很简单:在对a和b做欧几里德辗转相除时,所有中间的结果,包括最后得到的结果显然都有ua+vb的形式(比如第一步,假设a小于b,记a除b的结果为s,余数为t,即b=sa+t,则t=(-s)a+b,即u=-s,v=1)。而两个数互素意味着欧几里德辗转相除法的最后一步的结果是1,所以1也可以记作ua+vb的形式。稍微推广一点,如果(a,b)=c,那么存在u和v使得ua+vb=c(两边都除以c就回到原来的命题)。

再推广一点,如果A1,A2,……,An是n个整数,(A1,A2,…,An)=s,那么存在整数U1,U2,……,Un,使得

U1A1 + U2A2 + … + UnAn = s.    (*)

在代数学上称此结果为“整数环是主理想环”。这也不难证,只要看到

(A1,A2,A3,A4,…,An) = ((((A1,A2),A3),A4),…,An).

也就是说,可以反复应用上一段中的公式:比如三个数a,b,c,它们的最大公约数是d。假设(a,b)=e,那么(e,c)=((a,b),c)=d。现在有u1,u2使得u1a+u2b=e,又有v1,v2使得v1e+v2c=d,那么

(v1u1)a+(v1u2)b+(v2)c=d.

好,让我们回头看“灌水定理”。w是(A1,A2,…,An)的倍数,根据上节的公式(*),两边乘以这个倍数,我们就有整数V1,V2,……,Vn使得 V1A1 + V2A2 + … + VnAn = w.注意到Vi是有正有负的。

这就说明,只要分别把A1,A2,……,An壶,灌上V1,V2,……,Vn次(如果Vi是负的话,“灌上Vi次”要理解成“倒空-Vi次”),就可以得到w升水了。具体操作上,先求出各Vi,然后先往Vi是正数的壶里灌水,灌1次就把Vi减1。再把这些水到进Vi是负数的壶里,等某个壶灌满了,就把它倒空,然后给这个负的Vi加1,壶之间倒来倒去不变更各Vi的值。要注意的是要从池塘里灌水,一定要用空壶灌,要倒进池塘里的水,一定要是整壶的。这样一直到所有Vi都是0为止。

会不会发生卡住了,既不能灌水又不能倒掉的情况?不会的。如果有Vi仍旧是负数,而Ai壶却没满:那么如果有其它Vi是正的壶里有水的话,就都倒给它;如果有其它Vi是正的壶里没水,那么就拿那个壶打水来灌(别忘了给打水的壶的Vi减1);如果根本没有任何Vi是正的壶了——这是不可能的,这意味着w是负的。有Vi仍旧是正数,而Ai壶却没满的情况和这类似,你会发现你要用到定理中的条件1)。

这样“灌水定理”彻底得证。当然,实际解题当中如果壶的数目和容积都比较大的话,手工来找()中的各Ui比较困难,不过可以写个程序,连倒水的步骤都算出来。最后要指出的一点是,()中的Ui不是唯一的,所以倒水的方式也不是唯一的。

实现代码:

#include<cstdio>
#include<cstring>
#include<cmath>
#include<stack>
#include<vector>
#include<queue>
using namespace std;const int maxn = 1e5+10;
const int smaxn = 1010;
int table[smaxn][smaxn];
int front=0, last=0;
char strs[7][20]={"", "fill A\n", "fill B\n", "empty A\n", "empty B\n", "pour A B\n", "pour B A\n"};struct node{int a, b;int op;int pre;	//上一个状态
}tree[maxn];	//用数组的,线性存储结构来构造队列。node manipulate(node now, int way, int ca, int cb){int a = now.a;int b = now.b;node next;switch(way){case 1: a=ca; break;	//fill A case 2: b=cb; break;	//fill Bcase 3: a=0; break;	//empty A case 4: b=0; break;	//empty Bcase 5:if(a+b-cb>=0) {a=a+b-cb; b=cb;} //pour A B 判断溢出else {b=a+b; a=0;} break;case 6: if(a+b-ca>=0) {b=a+b-ca; a=ca;} //pour B A 判断溢出else {a=a+b; b=0;} break;}next.a = a;next.b = b;next.op = way;return next;
}int BFS(int ca, int cb, int n){//初始化node ans[100];memset(table, 0, sizeof(table));front=last=0;node S={0};//在tree的基础上,添加两个指针,实现队列的操作。采用线性存储结构来构造队列。//last指向队尾,front指向队首;//S入队列tree[last++] = S;//当队列为空时中断循环,即队首指针与队尾指针 重合。while(front != last){node now = tree[front];if(now.b == n) {return front;}table[now.a][now.b] =1; //标记当前状态 为 访问过,入队过。//对当前状态 执行6种操作,得到6个下一状态。for(int i=1; i<=6; i++){node next = manipulate(now, i, ca, cb);next.pre = front;if(table[next.a][next.b] == 0){tree[last++] = next;table[next.a][next.b] = 1; //标记当前状态 为 访问过。}//当前状态,如果不曾访问,曾入队列 待阅兵。反之跳过。if(next.b==n){break;}} //队首出列类比于pop(),以便得到队列的下一队首。front++;}return -1;
}void print(int index){if(index == -1) {//printf("FAIL\n"); return;}stack<int> st;//遍历操作序列,并入栈while(index != 0){st.push(tree[index].op);index = tree[index].pre;}while(!st.empty()){printf("%s", strs[st.top()]); st.pop();}printf("success\n");
}int main(){int ca, cb, n;while(scanf("%d%d%d", &ca, &cb, &n)!=EOF){print(BFS(ca, cb, n));}return 0;
}