有两个容量分别为 x升 和 y升 的水壶以及无限多的水。请判断能否通过使用这两个水壶,从而可以得到恰好 z升 的水?
如果可以,最后请用以上水壶中的一或两个来盛放取得的 z升 水。
你允许:
装满任意一个水壶
清空任意一个水壶
从一个水壶向另外一个水壶倒水,直到装满或者倒空
示例1:
输入: x = 3, y = 5, z = 4
输出: True
示例2:
输入: x = 2, y = 6, z = 5
输出: False
为简单起见,以 x = 1, y = 3, z = 2 为例,来看下如何构思:
当 x = 1, y = 3 时,一共有八种状态
(0,0), (1,0)
(0,1), (1,1)
(0,2), (1,2)
(0,3), (1,3)
既然时图的广度优先搜索,我们先来看下如何构图:
关于图片的说明:
·节点表示两个水壶的状态
·边表示操作方法:分别为倒满A/B,倒空A/B,A倒入B,B倒入A 六种方法。
·这是一个有向图,因为有些状态并不能护互为转移。比如 (1,1) 和 (1,0)。
简单介绍下BFS: 该过程总是从一个或若干个起始点开始,沿着边像水波一样逐层向外遍历,直到所有的点已被访问或者到达目标状态。编程上可以使用一个队列维护需要扩散的点,以及一个set或者数组维护已经被访问过的点来实现:
1.初始时,队列和set均为空。将起始点放入队列及set。
2.如果队列为空则 bfs 结束。
3.弹出队首元素并访问其周围元素,设为 p。
4.如果p为目标状态则 bfs 结束。
5.访问 p 周围的元素,将不在set中的元素放入队列及set。跳转第 2 步。
Java没有原生的Pair,所以我用Map.Entry替代了。leetcode上需要额外加上import语句。
class Solution {
int capacityX;
int capacityY;
public boolean canMeasureWater(int x, int y, int z) {
capacityX = x;
capacityY = y;
Set<Long> set = new HashSet<>();
Queue<Long> dfs = new LinkedList<>(); //遍历链表
dfs.add(0L);
while(!dfs.isEmpty()){
Long l = dfs.poll();
if(set.contains(l)){
continue;
}
x = (int)(l>>32);
y = (int)l.intValue();
if(x==z||y==z||x+y==z){
return true;
}
set.add(l);
if(x!=0&&y!=0){
dfs.add((long)y);
dfs.add(((long)x)<<32);
}
if(x!=capacityX){
dfs.add(combineToLong(capacityX,y));
if(y!=0){
dfs.add(combineToLong(Math.min(x+y,capacityX),Math.max(y-capacityX+x,0)));
}
}
if(y!=capacityY){
dfs.add(combineToLong(x,capacityY));
if(x!=0){
dfs.add(combineToLong(Math.max(x-capacityY+y,0),Math.min(x+y,capacityY)));
}
}
}
return false;
}
public long combineToLong(int a,int b){
long res = (long)a;
res = res<<32;
return res|b;
}
}
数学算法
这道题目要求的返回值是真或者假,并非求解倒水操作的集合。这时若是有数学方法能够算出是否有解,至少可以避免遍历所费的空间。
我们先来观察题目给出的例子,若是我们思考倒水的过程,可以像下方所示图来操作:
通过观察更多的方案,可以看出这些操作序列有以下几个特点:
每次注水,不会将两个水壶都打满,这样的情况在初始状态中即有所判断,即是否满足x+y==z
两个壶相互倒水的过程中,结果必然有一个空壶或者一个满壶,因为若是存在两个非空或非满的壶,则说明倒水的过程中没有刻度把控(只有全倒出,或者倒满才知道每个壶中水量的确定刻度)
每次装水的过程中,不会向非空壶内装水。若是有此操作,则相当于返回红框内的初始状态,或者两个壶都满
根据以上的观察,可以得出结论:两个壶内总水量每次的变化要么是x,要么是y,最终水量为ax+by(a、b为装水次数,若为负值,则说明该壶的水量都是从另一个壶中得来)
再回到题干,既然最终水量为ax+by,则只需判断是否存在a、b,满足:
ax + by = z
根据祖定理可知,判断该线性方程是否有解需要需要判断z是否为x,y最大公约数的倍数。此时为题转化为了求解最大公约数,而该问题可以使用gcd算法(辗转相除法)。最终代码如下
class Solution {
public boolean canMeasureWater(int x, int y, int z) {
if(x==z||y==z||x+y<=z){
if(x+y<z){
return false;
}
return true;
}
return x>y?(z%gcd(x,y))==0:(z%gcd(y,x))==0;
}
public int gcd(int x,int y){
return y==0?x:gcd(y,x%y);
}
}