点击此处快速跳到程序部分

水壶问题

有两个容量分别为 x升 和 y升 的水壶以及无限多的水。请判断能否通过使用这两个水壶,从而可以得到恰好 z升 的水?
如果可以,最后请用以上水壶中的一或两个来盛放取得的 z升 水。

你允许:
装满任意一个水壶
清空任意一个水壶
从一个水壶向另外一个水壶倒水,直到装满或者倒空

示例1: (From the famous "Die Hard" example)
输入: x = 3, y = 5, z = 4
输出: True示例2:
输入: x = 2, y = 6, z = 5
输出: False

第一印象

乍一看这道题目和题目的示例,以为直接用条件判断就能搞定,于是立刻写了如下非常错误的代码:

public boolean canMeasureWater(int x, int y, int z) {    if( (0.5*x + 0.5*y == z)||(x + y == z)||        (0.5*x + y == z)||(x + 0.5*y == z)||        (x + x == z)||(y + y == z) ){        return true;    }else{        return false;    }  }

当然执行的结果是错误的,这里主要烦了两个重要的错误:

  • 杯子只能倒空或倒满,不能倒一半(像脑筋急转弯那样)
  • 两个杯子,当然可以让水从一个倒入另一个

从上述一开始遇到的错误,可以引出此题的关键:

  • 需要不断将水倒来倒去,从而利用容量差得到更多样的容量的水
  • 为了减少实现复杂程度,输入的x,y应该按指定顺序,如前小后大
  • 对特定情况,能提前排除就提前排除,比如(0,1,2)

进入正题

首先,是问题的抽象,该问题可以转化为问一个三元组r(x,y,z),按一定规则对x,y进行运算,规则就是:

  • x,y只能被赋值不比自身大的数,
  • x,y运算过程中,只要有一个状态,或x,或y,或x+y,等于z,则问题得解
  • 问题无解的条件?

然后,由特殊推普通,实际计算几个例子,看看有什么规律,杯子x,y每次的状态有:

注意:下面的每一步都是注定的,即‘只有这样操作才对目标的获得有意义’,
目标是什么?目标就是每次操作都要‘利用容量差产生更多样的容量的水’。
x=4  y=54    5              //初始状态0    5              //小的倒空,大的倒满【问题I】4    5-(4-0)=1      //大的将小的倒满,大的会剩余,这里剩余 10    1              //小的倒空1    0              //大的中的剩余倒入小的1    5              //大的倒满4    5-(4-1)=2      //大的继续将小的倒满,大的还会剩余,这里剩余 20    2              //往复上述操作2    02    54    5-(4-2)=30    33    03    54    5-(4-3)=4      //x==y,后续会再重新回到开始状态,故这是终止状态【问题II】0    44    04    5
问题I,为什么小的倒空大的倒满?
因为如果相反,小的满,大的空,水只能从小杯子倒入大杯子,
且小杯子无剩余,故没有利用到容量差,所以这种顺序的操作时无意义的。 问题II,如果x和y不相等,且y>x即剩余的水多于小杯子的容量怎么办?
答:此时归结为【第二种情况】后续会讲到,所以当 y<=x 时可当做【第一种情况】

初步的编码实现

由上述思路作指导,很自然的想到了使用递归这种方式,于是有了以下代码:

class Solution {    public  int zx;     public  int sm;     public  int lg;     public  boolean MK = false;     public  boolean canMeasureWater(int x, int y, int z) {        if(z == x||z == y||z==x+y||z==0) {            return true;        }        if(z > x && z> y && z>(x+y)){            return false;        }         zx = z;        sm = x;        lg = y;        if(x>y){            int tp = sm;sm = lg;lg = tp;          }else{              recursion(sm,lg-(sm-0));        }        return MK;    }    public  void recursion(int x, int y){        if(zx == x+y || zx == x || zx==y)             MK = true;         if(x <= y||y>zx){            if(!MK) MK = false;            return;        }        x = 0;        if(zx == x+y || zx == x || zx==y)              MK = true;          int t = x;x = y;y = t;         x = x;        y = lg;        if(zx == x+y || zx == x || zx==y)              MK = true;         recursion(sm,lg-(sm-x));    }}

补全思路得到新的实现

上述代码不出意外的还是没过,解答出错,为什么呢,原来是原先的思路有问题,思考不全面,如下:

x=4  y=74    70    74    7-(4-0)=30    33    03    74    7-(4-3)=6      //上述步骤同先前同理0    6              //但此时出现 大的里的剩余 比小的容量还大【问题I】?    ?              ...
问题I 如果大杯子剩余的比小杯子容量大,怎么处理
答:此时应不同于一般情况,一般情况是大的里的剩余是比小的容量小
'所以剩余水可以倒入小的,但此时无法将大杯子的剩余水倒入小杯子,
因为会溢出,那么应该如何操作呢,本着利用‘容量差’的原则,所以:需要将小杯子用大杯子的剩余水倒满,此时大杯子的水仍有剩余,
如果此时剩余的水还比小杯子的大,那么先倒空小杯子,
然后继续用剩余的水装满小杯子,直到剩余的水 小于小杯子容量,此时停止,因为此时剩余的水可以倒入小杯子了,所以又回到了第一种情况。

下面,我们按刚才说的思路将先前的例子列完:

x=4  y=74    70    74    7-(4-0)=30    33    03    74    7-(4-3)=6      //此时,按刚才的思路【第二种情况】继续0    6              //小杯子倒空4    6-4=2          //用大杯子剩余的水倒满小杯子,大杯子还剩余 20    2              //此时回归【第一种情况】2    02    74    7-(4-2)=5      //此时,又回到【第二种情况】0    54    5-4=1          //此时又回归【第一种情况】0    11    01    74    7-(4-1)=4      //此时xy相等,如果继续,则状态回归初始,故此时终止

上述例子即完整的展现了正确的迭代步骤,即需要分为【两种情况】,所以在先前递归形式的基础上,对每次的迭代中的 x y的大小分情况进入不同的迭代入口即可。然而这一新的实现仍然出错误了:Stack Overflow!

如何避免递归的栈溢出

对于溢出时的测试用例:22003,31237,137,在我本机跑时递归了九千次左右就溢出停止了,但对于一般的小的测试用例,答案已经都是正确的了,所以此时的思路应是正确的,只是实现形式有问题,需要优化!

重新对上一章节中说到的例子进行考究,发现真正有用的(跟Z比较判断的)状态其实只有如下:

x=4  y=74    7-(4-0)=3  //【情况一】4    7-(4-3)=6       4    6-4=2      //【情况二】      4    7-(4-2)=5       4    5-4=1           4    7-(4-1)=4

优化情况二:按先前的思路可知,情况二其实不需要迭代操作,其就是在大杯子剩余水多于小杯子容量时,就拿小杯子每次都去装大杯子的剩余水,直到大杯子中剩余的水小于小杯子的容量,抽象为数学运算:

  • 取余 y % x == c

实现上,由于需要中间的每个状态(和z比较),所以可以用while循环来完成,优化后的代码:
【情况一】:大水杯里的剩水不断的增加,直到增加到剩水大于小水杯容量;
【情况二】:大水杯剩水不断的减少,直到剩水小于小水杯的容量;
再次明确:情况一时小水杯的容量 >= 大水杯的剩水; 情况二时小水杯的容量 < 大水杯的剩水;
注意:
对于每次迭代,都对两种情况分别对待,然后进行下一次迭代,此时迭代是靠递归驱动的。

class Solution {    public static int zx;     public static int sm; 

    public static int lgt;     public static boolean MK = false; 

    public static boolean canMeasureWater(int x, int y, int z) {        sm = x; lgt = y;        if(x > y){           sm = y; lgt = x;         }        zx = z;         recursion(lgt);        return MK;    }    //static int cnt = 0;    public static void recursion(int rs) {        if(sm + rs == zx|| sm + lgt == zx|| sm + rs == zx|| sm == zx|| rs == zx|| zx == 0){            MK = true;            return;        }        if(rs <= 0){            if(!MK) MK = false;            return;        }        if(sm <= rs){            int tmp = rs;            for(int i = 1; tmp >= sm;i ++){                tmp = tmp - sm;            }            recursion(tmp); //情况一        }else{

            int tmp = rs;            for(int i = 1; tmp < sm;i ++){                tmp = lgt - (sm - tmp);

            }            recursion(tmp); //情况一        }    }}

终极优化,递归的转化

上述代码仍然存在栈溢出的错误,所以还是递归的锅,显然,不是题目的测试样例刁钻,而是有些情况就是需要迭代几万次,即用递归是错误的实现方式。故转念一想,如何将递归转化为循环?

x=4  y=74    7-(4-0)=3  //【情况一】4    7-(4-3)=6       4    6-4=2      //【情况二】      4    7-(4-2)=5       4    5-4=1           4    7-(4-1)=4

在此回顾上述的内容,发现其实代码中的递归已经没有复杂的处理逻辑,且退化成了驱动迭代的一种结构,所以显然可以改成循环。由于原递归可以连续执行,所以转为循环理所应当的 最外层是 while(true)来制造连续迭代,然后循环的退出可以用return 或者break都可以,对于两种情况的处理还是要分别采用不同的实现,综上,去掉递归改用循环的代码:

public static boolean canMeasureWater(int x, int y, int z) {    if(z == 0) return true;    if((x == 0 || y == 0))        return (x + y == z);    if(x > y)        int t = x; x = y; y = t;    int rs = y;    while(true){        if(x + rs == z|| x + y == z|| x + rs == z|| x == z|| rs == z|| z == 0)            return true;        if(rs <= 0)            return false;        int tmp = rs;        if(x <= rs){            while(tmp >= x){                tmp = tmp - x;                //System.out.println((++cnt)+"\tb\tr("+x+","+tmp+")"+"\t"+z);                if(x + tmp == z|| x + y == z|| x + tmp == z|| x == z|| tmp == z|| z == 0){                    return true;                }            }            rs = tmp;        }else{            while(tmp < x){                tmp = y - (x - tmp);                if(x + tmp == z|| x + y == z|| x + tmp == z|| x == z|| tmp == z|| z == 0){                    return true;                }            }            rs = tmp;        }    }}

精益求精

上述代码终于是得到了绿绿的通过,然而其执行速度却是最慢的,查看代码其实发现在判断停止的条件的语句上过于冗余了,然后有些变量还做了无畏的赋值,于是继续精简,最后得到如下代码:

public static boolean canMeasureWater(int x, int y, int z) {    if(z == 0|| x + y == z)         return true;    if((x == 0 || y == 0))         return (x + y == z);    if(x > y){        int t = x; x = y; y = t;    }    int rs = y;      while(true){          if(rs <= 0)             return false;        if(x <= rs){            while(rs >= x){                rs = rs - x;                if( x + rs == z|| rs == z)                     return true;               }        }else{            while(rs < x){                rs = y - (x - rs);                if( x + rs == z|| rs == z)                     return true;            }        }    }}

上述代码的执行速度得到了不少的提升,由原先的21ms提升到了 8ms,如下图所示,现在下图提交的所有代码中,我的代码还成了第二个和第五个柱状图即8ms和21ms的样例程序,想想还有点小激动。

附第一梯队代码

当然,对于第一梯队的代码,使用到了 gcd() 函数对最大公约数进行求解,技巧性比较强,速度当然也快。相比之下,我这里其实相当于实现了一下gcd函数。但一般的小白(比如我)是很难将这个问题抽象成求解最大公约数的,所以我的思路其实更笨一点,但也更符合思考的逻辑。
下面是第一梯队的样例代码和解读。

class Solution {    public boolean canMeasureWater(int x, int y, int z) {        if (z==x+y || z==x || z==y || z==0)            return true;        if (x==0 || y==0 || z>x+y || z%gcd(x, y)!= 0)            return false;        return true;    }    private int gcd(int a, int b){        return b == 0? a : gcd(b, a%b);    }

}

此种题解的解题思路,转自网络

这道问题其实可以转换为有一个很大的容器,我们有两个杯子,容量分别为x和y,问我们通过用两个杯子往里倒水,和往出舀水,问能不能使容器中的水刚好为z升。那么我们可以用一个公式来表达:
z = m * x + n * y
其中m,n为舀水和倒水的次数,正数表示往里舀水,负数表示往外倒水,那么题目中的例子可以写成: 4 = (-2) * 3 + 2 * 5,即3升的水罐往外倒了两次水,5升水罐往里舀了两次水。那么问题就变成了对于任意给定的x,y,z,存不存在m和n使得上面的等式成立。根据裴蜀定理,ax + by = d的解为 d = gcd(x, y),那么我们只要只要z % d == 0,上面的等式就有解,所以问题就迎刃而解了,我们只要看z是不是x和y的最大公约数的倍数就行了,别忘了还有个限制条件x + y >= z,因为x和y不可能称出比它们之和还多的水。


由于这道题前前后后想了两天多,且期间不断地修正自己的想法,修改对应的实现,最后AC。虽然只是一道普通的题目,但以前还真没真真正正的不借助参考的独立思考过这样难度的题目,所以就在这里啰嗦了一大堆,看来以后还是得多独立思考,正应那句话:“不是不会做,而是不去想” 啊!

.wp-r{font-size: 20px!important;font-weight: 700!important;}

两个水壶相互倒水—水壶问题相关推荐

  1. H - Pots POJ - 3414(两个锅互相倒水)

    H - Pots POJ - 3414 题意: (两个锅互相倒水)希望能通过题目中的几种操作使一个锅中的水量为目标水量 bfs 搜索每一步的每种操作,直到所有操作用完,则impossible,否则输出 ...

  2. 复习--3--对于第三堂课的总结--将两个页面相互用超链接链接到一起

    1.空格  -- --  在HTML文件中是使用键盘的空格键无论打多少个都只会显示一个,如使用全角输入法即输入法自带的长空格,那样 不规范容易出错 2.在文本中出现<> <用 &am ...

  3. 两个弹窗相互切换(安卓苹果通用方法)

    两个弹窗相互切换(安卓苹果通用方法) 效果 1-点击问号一弹出第一个对话框,第二个对话框消失 2-点击问号二弹出第二个对话框,第一个对话框消失 3-点击其他地方,两个对话框都消失 做法: (1)设置一 ...

  4. Cocos Creator两个类相互引用(调用)

    如果两个类相互引用,脚本加载阶段就会出现循环引用,循环引用将导致脚本加载出错: ///Game.js var Item = require("Item"); var Game = ...

  5. cocos 时间函数需要什么引用_Cocos Creator两个类相互引用(调用)

    如果两个类相互引用,脚本加载阶段就会出现循环引用,循环引用将导致脚本加载出错: ///Game.js var Item = require("Item"); var Game = ...

  6. Qt使用两组RadioButton,两组之间相互独立

    Qt中使用两组共四个RadioButton时,由于RadioButton的特性,所以两组一共四个按钮每次只能选择一个,要使得两组RadioButton相互独立,需要用到QButtonGroup这个功能 ...

  7. Tekla二次开发使用Tekla API 将两个零件相互切割

    关注"闭目鸽"微信公众号回复"tekla"关键字, 便可获得数GB的精品tekla视频教程 Tekla二次开发使用Tekla API 将两个零件相互切割的代码 ...

  8. 计算机硬盘对考,在win7系统电脑中如何实现两块硬盘相互对拷

    大家都知道硬盘是数据存储设备和数据交换媒介,在win7系统电脑中有什么办法可以将一块硬盘资料转移到另外一块硬盘上呢?实现两块硬盘相互对拷?GHOST克隆是不二的选择,但是对于GHOST这个软件很多朋友 ...

  9. 两个类相互包含引用的问题--类前向声明

    在构造自己的类时,有可能会碰到两个类之间的相互引用问题,例如:定义了类A类B,A中使用了B定义的类型,B中也使用了A定义的类型 class A {     int i;     B b; } clas ...

最新文章

  1. Android 之数据传递小结
  2. 【ffmpeg】基本使用方法总结
  3. 安装配置优化nginx
  4. Python-web框架 fastapi
  5. Linux下常用操作汇总
  6. 编辑请求内容 Charles
  7. 破解Visio时office失效,激活失败
  8. delphi获取本机IP地址
  9. 【CVPR-2019】基于深度学习优化光照的暗光图像增强
  10. 【玩转微信公众平台之中的一个】序章(纯粹扯淡)
  11. 解决SSH连接超时的2个配置方法
  12. c语言共阴极数码管编码,数码管之共阴极与共阳极编码
  13. Python 理解 精灵 和 精灵组
  14. Win11 专业工作站版安装安卓子系统方法 (离线包安装)
  15. mysql修改初始化得到的密码
  16. python的内置数据结构_Python基础知识2-内置数据结构(上)
  17. 数据仓库介绍:什么是数据仓库、数据仓库功能、数据仓库价值、数仓领域职业发展方向规划
  18. 忆恩师刘自朗,我的高中物理老师
  19. 免费小红伞9.0在服务器中的安装办法
  20. P1157 组合的输出

热门文章

  1. 一种全国产化军用计算机设计大赛,北科大新闻网-我校多支团队在中国大学生计算机设计大赛全国总决赛中斩获佳绩...
  2. 计算机桌面壁纸小,电脑的桌面壁纸大小怎么设置
  3. 中国移动,电信,联通,铁通,网通的区别与联系
  4. C++ __builtin_函数
  5. 有什么样的将军就有什么样的兵
  6. Linux的man中文帮助手册
  7. openGL, mac 上 glad 的环境搭建
  8. 实验11-1-7 藏头诗 (15 分)
  9. python+selenium模拟163邮箱登录
  10. java微信扫码支付_java 微信扫码支付 示例代码