leetcode-水壶问题

 题目来自LeetCode,链接:水壶问题。具体描述为:有两个容量分别为x升和y升的水壶以及无限多的水。请判断能否通过使用这两个水壶,从而可以得到恰好z升的水?如果可以,最后请用以上水壶中的一或两个来盛放取得的z升水。你允许:

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

 示例1:

输入: x = 3, y = 5, z = 4
输出: True

 示例2:

输入: x = 2, y = 6, z = 5
输出: False

 首先,可以用一个二元组(X中水量,Y中水量)表示在任意一个时刻的状态。每个时刻都有以下几个可选操作:

  • 把X的水倒进Y,直至Y满或X空;
  • 把Y的水倒进X,直至X满或Y空;
  • 把X装满;
  • 把Y装满;
  • 把X倒空;
  • 把Y倒空。

 因此,本题可以使用深度优先搜索BFS来解决。在每一步搜索时,依次尝试上述所有的操作,递归地搜索下去。同时还需要用一个哈希表存储所有已经搜索过的状态,从而保证每个状态至多只被搜索一次。

 JAVA版代码如下:

class Solution {
    public boolean canMeasureWater(int x, int y, int z) {
        Set<String> set = new HashSet<>();
        Queue<int[]> queue = new LinkedList<>();
        queue.offer(new int[] {0, 0});
        while (!queue.isEmpty()) {
            int[] item = queue.poll();
            if (item[0] == z || item[1] == z || item[0] + item[1] == z) {
                return true;
            }
            if (!set.add(item[0] + "," + item[1])) {
               continue;
            }
            queue.offer(new int[] {x, item[1]});
            queue.offer(new int[] {item[0],y});
            queue.offer(new int[] {0, item[1]});
            queue.offer(new int[] {item[0], 0});
            queue.offer(new int[] {Math.max(0, item[0] - (y - item[1])), Math.min(item[1] + item[0], y)});
            queue.offer(new int[] {Math.min(item[0] + item[1], x), Math.max(0, item[1] - (x - item[0]))});
        }
        return false;
    }
}

 这个直接就超时了,因为状态还是有点太多了啊。

 然后参考评论区的一种简单BFS。这里不去关注(X中水量,Y中水量)这个二元状态,改为关注总水量n。那么每次操作只会让桶里的水总量增加 x,增加 y,减少 x,或者减少 y。原因在于在题目所给的操作下,两个桶不可能同时有水且不满。

  • 首先,每次操作后都至少有一个桶是空的或者满的。
  • 其次,对一个不满的桶加水是没有意义的。因为如果另一个桶是空的,那么这个操作的结果等价于直接从初始状态给这个桶加满水;而如果另一个桶是满的,那么这个操作的结果等价于从初始状态分别给两个桶加满。所以这种增加量不是x或y的操作是无意义的。
  • 再次,把一个不满的桶里面的水倒掉是没有意义的。因为如果另一个桶是空的,那么这个操作的结果等价于回到初始状态;而如果另一个桶是满的,那么这个操作的结果等价于从初始状态直接给另一个桶倒满。所以这种减少量不是x或y的操作也是无意义的。

 有了上面的分析,现在我们就只有四个状态了,同样可以用BFS:

  • 增加x:相当于给X加满水,所以须满足n \le y以将全部水先转移到Y;
  • 增加y:相当于给Y加满水,所以须满足n \le x以将全部水先转移到X;
  • 减少x:相当于倒掉X的水,所以须满足n \ge x以能先装满X再倒掉;
  • 减少y:相当于倒掉Y的水,所以须满足n \ge y以能先装满Y再倒掉。

 JAVA版代码如下:

class Solution {
    public boolean canMeasureWater(int x, int y, int z) {
        Set<Integer> set = new HashSet<>();
        Queue<Integer> queue = new LinkedList<>();
        queue.offer(0);
        while (!queue.isEmpty()) {
            int n = queue.poll();
            if (n == z) {
                return true;
            }
            if (n <= y && set.add(n + x)) {
                queue.offer(n + x);
            }
            if (n <= x && set.add(n + y)) {
                queue.offer(n + y);
            }
            if (n >= x && set.add(n - x)) {
                queue.offer(n - x);
            }
            if (n >= y && set.add(n - y)) {
                queue.offer(n - y);
            }
        }
        return false;
    }
}

 提交结果如下:


 最后是数学解法。因为可以认为每次操作只会给水的总量带来x或者y的变化量,所以可以把目标改写成:找到一对整数 a,和b使得 a x + b y = z ax+by=z 。而只要满足 z x + y z\leq x+y ,且这样的a和b存在,那么就可以达成目标。这是因为:

  • a 0 a\geq 0 b 0 b\geq 0 ,那么显然可以通过倒满a次X和b次Y达成目标。

  • a < 0 a\lt 0 ,那么可以进行以下操作:

    • 往Y倒水;
    • 把Y的水倒入X;
    • 如果Y不为空,那么X肯定是满的,把X倒空,然后再把Y的水倒入X以保证Y是空的。

  重复以上操作直至某一步时将X倒空了a次,给Y倒了b次水。

  • b < 0 b\lt 0 ,方法同上,x 与 y 互换。

 然后就需要用到数学定理了,根据贝祖定理, a x + b y = z ax+by=z 有解当且仅当z是x和y的最大公约数的倍数。因此我们只需要找到 x和y的最大公约数并判断z是否是它的倍数即可。

 JAVA版代码如下:

class Solution {
    private int gcd(int x, int y) {
        while (true) {
            if (x % y == 0) {
                return y;
            }
            else {
                int temp = y;
                y = x % y;
                x = temp;
            }
        }
    }
    public boolean canMeasureWater(int x, int y, int z) {
        if (x + y < z) {
            return false;
        }
        if (x == 0 || y == 0) {
            return z == 0 || z == x + y;
        }
        int n = x > y ? gcd(x, y) : gcd(y, x);
        return z % n == 0;
    }
}

 提交结果如下:


发布了68 篇原创文章 · 获赞 9 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/JR_Chan/article/details/105012878