智力题:倒水问题
倒水问题1:
利用给定容量的两个容器得到指定目标的多少升水?不对借用第三个容器
这个题目的版本非常之多,有微软版的,腾讯版的,新浪版的等等,最常见的是给你一个容量为5升的桶和一个容量为3升的桶,水不限使用,要求精确得到4升水。
解法一:穷举法
解法肯定有很多,可以用宽度优先搜索(BFS),也可以用穷举法。穷举法实现比较方便,其基本思想是用:用小桶容量的倍数对大桶的容量进行取余。比如3升的桶和5升的桶得到4升水可以这样做:
3 % 5 = 3
6 % 5 = 1
9 % 5 = 4
成功得到4升水。(PS:上面的过程用如何用文字描述了?)
同样,用7升的桶和11升的桶得到2升水可以这样做:
7 % 11 = 7
14 % 11 = 3
21 % 11 = 10
28 % 11 = 6
35 % 11 = 2
成功得到2升水。
哈哈,有了这个基本思想在用笔算答案时简直是遇神杀神,遇佛杀佛,又方便又快速!如果要求用程序来实现如何做了?easy,将倒水问题的基本思想用易于编程的话来翻译下——不断用小桶装水倒入大桶,大桶满了立即清空,每次判断下二个桶中水的容量是否等于指定容量。有了这个倒水问题的编程指导方针后代码非常容易写出:
//热门智力题 - 打水问题//基本思想:用小桶容量的倍数对大桶的容量进行取余。//指导方针:不断用小桶装水倒入大桶,大桶满了立即清空,//每次判断下二个桶中水的容量是否等于指定容量。#include<iostream>#include <vector>#include<string>using namespace std;const string OPERATOR_NAME[7] = {"装满A桶","装满B桶","将A桶清空","将B桶清空","A桶中水倒入B桶","B桶中水倒入A桶","成功"};int main(){cout<<"热门智力题 - 打水问题"<<endl;cout<<" --by MoreWindows( http://blog.csdn.net/MoreWindows )--\n"<<endl;int a_volume, b_volume, goal_volume;vector<string> record; //记录操作步数int ai;int i, a_water, b_water;cout<<"请输入A桶容量,B桶容量,目标容量:";cin>>a_volume>>b_volume>>goal_volume;a_water = b_water = 0; //A桶,B桶中有多少升水char szTemp[30];while (true){if (a_water == 0)//A桶没水,就装满水{a_water = a_volume;sprintf(szTemp, " A:%d B:%d", a_water, b_water);record.push_back(OPERATOR_NAME[0] + szTemp);//fill A}else{//如果A桶的水比(B桶容量-B桶的水)要多if (a_water > b_volume - b_water){//A桶的水==A桶的水+B桶的水-B桶容量a_water = a_water + b_water- b_volume;b_water = b_volume; //B桶的水装满了sprintf(szTemp, " A:%d B:%d", a_water, b_water);record.push_back(OPERATOR_NAME[4] + szTemp);//A->Bif (a_water == goal_volume)break;b_water = 0; //将B桶清空sprintf(szTemp, " A:%d B:%d", a_water, b_water);record.push_back(OPERATOR_NAME[3] + szTemp);}else{//B桶的水==A桶的水+B桶的水b_water += a_water;a_water = 0;sprintf(szTemp, " A:%d B:%d", a_water, b_water);record.push_back(OPERATOR_NAME[4] + szTemp);//A->Bif (b_water == goal_volume)break;}}}record.push_back(OPERATOR_NAME[6]); //successcout<<"\n---------------------------------------------------"<<endl;cout<<"一个可行的倒水方案如下"<<endl;vector<string>::iterator pos;for (pos = record.begin(); pos != record.end(); pos++)cout<<*pos<<endl;cout<<"---------------------------------------------------"<<endl;return 0;}
程序运行结果如下:
注意这里只是给出一个可行的倒水方案,不一定是最优解。另外倒水问题要注意下像用2升的桶和4升的桶得到3升水这种不可解的情况,这种不可解情况在用本文中对倒水问题所总结的基本思想计算时会得到循环数列。其根本原因是二个桶容量的最大公约数无法被目标容量所整除(根据欧几里德算法可知,下一个问题判断倒水问题是否有解),如6升的桶和9升的桶无法得到2升水是因为6和9的最大公约数是3即GCD(6,9)=3而3无法整除2。
转载请标明出处,原文地址:http://blog.csdn.net/morewindows/article/details/7481851
解法二:宽度优先搜索(BFS)
思路:
总状态只有那么多, 反正每一步后都只有 6 种操作明显的 BFS 关键是每一步路径的记录。
开始用 BFS + 容器做了一遍,发现还是无法处理好输出路径问题,只好重新开始用数组模拟。
容器虽然很好用又方便,但是在不考虑内存的状况下,效率终究比不上数组模拟的了。
注意到 A 和 B 的范围是 1 到 100 的整数,
那么我们可以用vis[i][j]来记录每一种状态 0 <= i, j <= 100 ;
i 表示目前 A 容器中的水, j 表示目前 B 容器中的水
应该很容易就能分析出,对应于每一种状态的下一步只有六种情况:
一:用水池中的水装满 A
二:用水池中的水装满 B
三:把 A 中的水全部倒进废水池
四:把 B 中的水全部倒进废水池
五:把 A 中的水倒进 B 【不能溢出】
那么对应可能会有两种状态:用 k1 表示 A 中的水, k2 表示 B 中的水
如果 k1+k2 <= B 则 k2 = k1+k2; k1 = 0 【不能够装满容器 B】注意顺序
否则 k1 = k1+k2-B; k2 = B 【能够装满容器 B】
六:把 B 中的水倒进 A 【不能溢出】
也有两种情况,分析同上
如果 k1+k2 <= A 则 k1 = k1+k2; k2 = 0;
否则 k2 = k1+k2-A; k1 = A
用结构体数组来模拟队列
用 k1,k2 来记录当前容器中水的状态
前面已经分析过对应于每种情况只有 6 种操作, 那么对应每种情况的操作记录为 1 到 6 输出时处理下就好了。
当然少不了记录到当前状态最少用了多少步数 step
因为要记录路径,所以定义一个 pre 来记录上一步在数组模拟队列中的下标。
最后如果能够达到目的,在判定最后一步的时候记录下最后一步在数组中的编号 lastIndex
然后从lastIndex从后往前找【pre】前一个步骤在数组中的编号存在 id[] 中
最后再按照扫描出的路径依次遍历即可。
#include<stdio.h>#include<string.h>const int maxn = 110;int vis[maxn][maxn]; //标记状态是否入队过int a,b,c; //容器大小int step; //最终的步数int flag; //纪录是否能够成功/* 状态纪录 */struct Status{int k1,k2; //当前水的状态int op; //当前操作int step; //纪录步数int pre; //纪录前一步的下标}q[maxn*maxn];int id[maxn*maxn]; //纪录最终操作在队列中的编号int lastIndex; //最后一个的编号void bfs(){Status now, next;int head, tail;head = tail = 0;q[tail].k1 = 0; q[tail].k2 = 0;q[tail].op = 0; q[tail].step = 0; q[tail].pre = 0;tail++;memset(vis,0,sizeof(vis));vis[0][0] = 1; //标记初始状态已入队while(head < tail) //当队列非空{now = q[head]; //取出队首head++; //弹出队首if(now.k1 == c || now.k2 == c) //应该不会存在这样的情况, c=0{flag = 1;step = now.step;lastIndex = head-1; //纪录最后一步的编号}for(int i = 1; i <= 6; i++) //分别遍历 6 种情况{if(i == 1) //fill(1){next.k1 = a;next.k2 = now.k2;}else if(i == 2) //fill(2){next.k1 = now.k1;next.k2 = b;}else if(i == 3) //drop(1){next.k1 = 0;next.k2 = now.k2;}else if(i == 4) // drop(2);{next.k1 = now.k1;next.k2 = 0;}else if(i == 5) //pour(1,2){if(now.k1+now.k2 <= b) //如果不能够装满 b{next.k1 = 0;next.k2 = now.k1+now.k2;}else //如果能够装满 b{next.k1 = now.k1+now.k2-b;next.k2 = b;}}else if(i == 6) // pour(2,1){if(now.k1+now.k2 <= a) //如果不能够装满 a{next.k1 = now.k1+now.k2;next.k2 = 0;}else //如果能够装满 b{next.k1 = a;next.k2 = now.k1+now.k2-a;}}next.op = i; //纪录操作if(!vis[next.k1][next.k2]) //如果当前状态没有入队过{vis[next.k1][next.k2] = 1; //标记当前状态入队next.step = now.step+1; //步数 +1next.pre = head-1; //纪录前一步的编号//q.push(next);//q[tail] = next; 加入队尾q[tail].k1 = next.k1; q[tail].k2 = next.k2;q[tail].op = next.op; q[tail].step = next.step; q[tail].pre = next.pre;tail++; //队尾延长if(next.k1 == c || next.k2 == c) //如果达到目标状态{flag = 1; //标记成功step = next.step; //纪录总步骤数lastIndex = tail-1; //纪录最后一步在模拟数组中的编号return;}}}}}int main(){while(scanf("%d%d%d", &a,&b,&c) != EOF){flag = 0; //初始化不能成功step = 0;bfs();if(flag){printf("%d\n", step);id[step] = lastIndex; //最后一步在模拟数组中的编号for(int i = step-1; i >= 1; i--){id[i] = q[id[i+1]].pre; //向前找前一步骤在模拟数组中的编号}for(int i = 1; i <= step; i++){if(q[id[i]].op == 1)printf("FILL(1)\n");else if(q[id[i]].op == 2)printf("FILL(2)\n");else if(q[id[i]].op == 3)printf("DROP(1)\n");else if(q[id[i]].op == 4)printf("DROP(2)\n");else if(q[id[i]].op == 5)printf("POUR(1,2)\n");else if(q[id[i]].op == 6)printf("POUR(2,1)\n");}}else printf("impossible\n");}return 0;}
结果:
倒水问题是否有解判断
题目:
有两个容器,容积分别为A升和B升,有无限多的水,现在需要C升水。 我们还有一个足够大的水缸,足够容纳C升水。起初它是空的,我们只能往水缸里倒入水,而不能倒出。 可以进行的操作是: 把一个容器灌满; 把一个容器清空(容器里剩余的水全部倒掉,或者倒入水缸); 用一个容器的水倒入另外一个容器,直到倒出水的容器空或者倒入水的容器满。 问是否能够通过有限次操作,使得水缸最后恰好有C升水。
输入:三个整数A, B, C,其中 0 < A , B, C <= 1000000000 输出:0或1,表示能否达到要求。
函数头部: c语言:1表示可以,0表示不可以 int can(int a,int b,int c);
c++语言: true表示可以,false表示不可以 bool can(int a,int b,int c);
java语言:true表示可以,false表示不可以 public class Main {public static boolean can(int a,int b,int c); }
分析
经典的倒水问题,有不少公司也出了类似的面试题目,有的以选择题形式出现,也有编程题形式出现的,下面做简要的分析:
对题目做简要的处理分析后,C升水是可以分多次倒入的,假设A > B,那么每次可以倒的水的量为A , B , (A + B) ,( A - B)升水,设置4个因子,分别为x1 , x2, x3, x4 , (x1 , x2, x3, x4 属于整数),如果可以使得水缸最后恰好有C升水,那么必然存在整数x1 , x2, x3, x4,使得
Ax1 + Bx2 + (A + B)x3 + (A - B)x4 = C 等式成立;
对等式做一定的变换,得到公式
(x1 + x3 + x4)A + (x2 + x3 - x4)B = C ; --( 1-1 )
设 x = x1 + x3 + x4 , y = x2 + x3 - x4 , x, y 均为整数;最终得到公式
xA + yB = C ; --( 1-2 )
x1 , x2, x3, x4 可以假设为正整数,用几个for循环可以实现,但是时间复杂度太大,为O(N4),题目中给的范围是0 < A , B, C <= 1000000000;整数在十亿范围,显然运行时间肯定会超过 3s ,不符合要求,那有没有更加合适的方法呢,在算法的书里面,有一个算法,与公式( 1-2 ) 不谋而合,是扩展的欧几里德算法,算法描述:
定理:
对于不完全为 0 的非负整数 a,b,gcd(a,b)表示 a,b 的最大公约数,必然存在整数对 x,y ,使得 gcd(a,b)=ax+by.
根据欧几里德扩展算法,Gcd(A, B) = Ax + By,求出A和B的最大公约数,如果C能被最大公约数整除Gcd(A, B) 整除,那就可以实现水缸里恰好为C升水;
那题目就直接转换为求A 、B的最大公约数了,求公约数可以用辗转相除法,代码如下:
#include <stdio.h>#include <stdlib.h>//求最大公约数int gcd(int a, int b){int m = a, n = b , r = 1;while(1){r = m % n;if(r == 0){return n;}else{m = n;n = r;}}}//返回值1表示能使得水缸恰好有C升水,0表示不能int can(int a,int b,int c){int result = 0;result = gcd(a,b);if(c % result == 0 ){return 1;}else{return 0;}}int main(void){int A , B , C;A = 1234567;B = 7654321;C = 9999999;printf("the result is %d",can(A,B,C));return 0;}
同样,附带几个测试用例:
输入:A = 1234567, B = 7654321 , C = 9999999, 输出:result = 1;
输入:A = 9999, B = 5555, C = 2222, 输出:result = 1;
输入:A = 1000000000, B = 2, C = 1 , 输出:result = 0.
下面是做一个实例演示:假设A = 11 , B =39 , C = 2,返回值为1,说明可以实现,为方便叙述,采用A(11) , B(39)表示容器,步骤如下:
1、 将容器 B(39) 倒满水,然后3次倒入 A(11) 容器中,那么 B(39) 剩下 39 - 11 * 3 = 6升水,此时A(11)可用;
2、 把 B(39) 中的 6 升水全部倒入容器 A(11) 中,那么容器 A(11) 中有 6 升水,5 升是空的,此时B(39)可用;
3、 把 B(39) 倒满水,然后往第2步得到的 A(11 )倒入直到 A(11) 满为止,那B(39)剩下 39 - 5 = 34 升水,清空 A(11) ,此时A(11)可用;
4、 步骤3得到的 B(39) 容器有34升水,3次倒入 A(11)中,那 B(39)中剩下 34 - 11 * 3= 1升水,此时 A(11)可用;
5、 把步骤4的 1 升水倒入水缸,清空 A(11) 和 B(39),重做步骤1 - 4,再往水缸倒入1升水,那水缸里就是 1 + 1 = 2 升水了。
作者:大卫david 原创作品,如需转载,请与作者联系。否则将追究法律责任。


浙公网安备 33010602011771号