容器取水
一个经典的问题:使用容量为3升和5升的水桶取出4升的水,只可以进行如下操作: - 装满任意一个水桶; - 清空任意一个水桶; - 从一个水桶向另外一个水桶倒水,直到装满或者倒空;
方法为:5升桶装满>>>倒到3升桶>>>3升桶倒掉>>>5升桶剩下的2升倒到3升桶>>>5升桶装满>>>5升桶往装着2升水的3升桶倒入1升>>>3升桶的水倒掉。最后在5升桶中还剩下4升水。
把这个问题推广到更普遍的形式:有两个水壶,容量分别为 Capacity 1和 Capacity 2升。水的供应是无限的。确定是否有可能使用这两个壶准确得到 targetCapacity 升。倒水操作和之前所述一致。
(lc365)
暴力遍历
使用BFS暴力枚举各种情况,枚举过程需要记录已经出现过的水总量,遍历到已出现过的总量略过,到最后队列为空还未出现目标总量则说明无法取到相应容量的水。(开始时两个桶总量无法达到目标值则不可能达到)
1 | def canMeasureWater(Capacity1: int, Capacity2: int, targetCapacity: int) -> bool: |
迭代遍历
观察规律可以发现,可以通过不断使用小桶倒水,大桶加水的方式来得到新的水总量。即设大桶容量为a小桶容量为b,初始可得a+b的水,通过小桶不断倒水,可以获取a-b,a-2b...a-kb
的水,当水总量不足小桶容量时,往大桶加水,又可以得出2a-kb,2a-(k+1)b...
最终我们可以获取ma-nb
的水,当ma等于nb等于a和b的最小公倍数时,水总量为0,即将进入下一个循环,所以在此期间为找到目标水量,则无法得到目标水量。
1 | def canMeasureWater(x: int, y: int, z: int) -> bool: |
数学推导
我们认为,每次操作只会让桶里的水总量增加 x,增加 y,减少 x,或者减少 y。证明:
首先要清楚,在题目所给的操作下,两个桶不可能同时有水且不满。因为观察所有题目中的操作,操作的结果都至少有一个桶是空的或者满的; 其次,对一个不满的桶加水是没有意义的。因为如果另一个桶是空的,那么这个操作的结果等价于直接从初始状态给这个桶加满水;而如果另一个桶是满的,那么这个操作的结果等价于从初始状态分别给两个桶加满; 再次,把一个不满的桶里面的水倒掉是没有意义的。因为如果另一个桶是空的,那么这个操作的结果等价于回到初始状态;而如果另一个桶是满的,那么这个操作的结果等价于从初始状态直接给另一个桶倒满。 因此,我们可以认为每次操作只会给水的总量带来 x 或者 y 的变化量。因此我们的目标可以改写成:找到一对整数 a, b,使得\(ax+by=z\)
而只要满足 \(z\leq x+y\),且这样的 a, b存在,那么我们的目标就是可以达成的。这是因为:
若 \(a\geq 0, b\geq 0\),那么显然可以达成目标。
若$ a< 0$,那么可以进行以下操作:
往 y 壶倒水;
把 y 壶的水倒入 x 壶;
如果 y 壶不为空,那么 x 壶肯定是满的,把 x 壶倒空,然后再把 y 壶的水倒入 x 壶。
重复以上操作直至某一步时 x 壶进行了 a 次倒空操作,y 壶进行了 b 次倒水操作。
若 \(b\lt 0\),方法同上,x 与 y 互换。
而贝祖定理告诉我们,\(ax+by=z\)有解当且仅当 z 是 x, y的最大公约数的倍数。因此我们只需要找到 x, y 的最大公约数并判断 z 是否是它的倍数即可。
1 | def canMeasureWater(self, x: int, y: int, z: int) -> bool: |