LeetCode 365 水壶问题:两种解法深度解析(DFS + 数学贝祖定理)

112 阅读2分钟

🧩 一、题目回顾:水壶问题到底在问什么?

题目链接LeetCode 365. 水壶问题

✅ 题意描述

image.png

💡 二、核心洞察:操作的本质是什么?

我们来思考每一次操作对“总水量”的影响:

操作对总水量的影响
装满 X+X
装满 Y+Y
清空 X-X
清空 Y-Y
倒水(X→Y 或 Y→X)总量不变(只是转移)

👉 所以,只有装满和清空会改变总水量,而倒水只是内部调配。

因此,整个过程的总水量始终是形如:

ax+bya \cdot x + b \cdot y

其中 a,ba, b 是整数(正表示加水,负表示倒掉)。最终我们要判断是否存在整数 a,ba, b,使得:

ax+by=za \cdot x + b \cdot y = z

这正是经典的线性不定方程求解问题


🌲 三、思路一:深度优先搜索(DFS)——暴力穷举所有状态

✅ 核心思想

既然无法直接看出数学规律,我们可以采用最直观的方式:枚举所有可能的状态组合

每个状态由 (remainX, remainY) 表示,即当前两个水壶中的水量。

  • 初始状态:(0, 0)
  • 目标状态:满足 remainX == zremainY == zremainX + remainY == z
  • 使用栈或队列进行遍历(这里使用栈实现迭代式 DFS)
  • 用集合记录已访问状态,避免重复和死循环

🔁 六种合法操作的状态转移

从任意状态 (a, b) 可以转移到:

操作新状态
装满 X(x, b)
装满 Y(a, y)
清空 X(0, b)
清空 Y(a, 0)
X 向 Y 倒水(a - pour, b + pour)pour = min(a, y - b)
Y 向 X 倒水(a + pour, b - pour)pour = min(b, x - a)

💻 JavaScript 实现(迭代 DFS)

var canMeasureWater = function(x, y, z) {
    // 边界情况:总量不够
    if (x + y < z) return false;

    const stack = [[0, 0]];
    const visited = new Set();

    while (stack.length > 0) {
        const [remainX, remainY] = stack.pop();
        
        // 检查是否达成目标
        if (remainX === z || remainY === z || remainX + remainY === z) {
            return true;
        }

        const key = `${remainX}#${remainY}`;
        if (visited.has(key)) continue;
        visited.add(key);

        // 推入六种新状态
        stack.push(
            [x, remainY],           // 装满 X
            [remainX, y],           // 装满 Y
            [0, remainY],           // 清空 X
            [remainX, 0],           // 清空 Y
            [remainX - Math.min(remainX, y - remainY), remainY + Math.min(remainX, y - remainY)], // X→Y
            [remainX + Math.min(remainY, x - remainX), remainY - Math.min(remainY, x - remainX)]  // Y→X
        );
    }

    return false;
};

⏱️ 复杂度分析

  • 时间复杂度:O(X × Y),最多 (x+1)*(y+1) 种状态
  • 空间复杂度:O(X × Y),用于存储已访问状态

❗ 局限性

xy 很大时(比如 10910^9),状态空间爆炸,程序会超时或内存溢出。
所以这个方法只适用于小数据场景。


📐 四、思路二:数学方法——贝祖定理(Bézout's Identity)

✅ 核心理论:贝祖定理

对于整数 xxyy,存在整数 aabb 使得:

ax+by=gcd(x,y)a \cdot x + b \cdot y = \gcd(x, y)

更一般地,方程 ax+by=za \cdot x + b \cdot y = z 有整数解的充要条件是:

zmodgcd(x,y)=0z \mod \gcd(x, y) = 0

换句话说:只要 z 是 gcd(x, y) 的倍数,就可以通过若干次“加减 x”和“加减 y”构造出来。

但这还不够!我们必须结合实际物理限制。


🔒 加入边界条件:现实世界的约束

即使数学上可行,也要考虑水壶的实际容量限制:

  1. 总水量不能超过两壶容量之和
    → 若 z > x + y,一定不可能实现。

  2. 特殊情况处理:x 或 y 为 0

    • 如果 x === 0 && y === 0,则只能得到 0 升水;
    • 如果其中一个为 0,则只能得到 0 或非零的那个容量。
  3. z === 0 的情况总是成立(什么都不做即可)


✅ 最终判断逻辑

综合以上分析,答案成立的充要条件是:

(z <= x + y) && (z % gcd(x, y) === 0)

再加上特判 xy 为 0 的情况。

💻 数学法代码实现(推荐解法)

var canMeasureWater = function(x, y, z) {
    // 1. 总量超出最大容量
    if (x + y < z) return false;

    // 2. z 为 0 总是可以达成
    if (z === 0) return true;

    // 3. 处理 x 或 y 为 0 的情况
    if (x === 0 || y === 0) {
        return z === x || z === y; // 只能取其中一个的容量或 0
    }

    // 4. 计算最大公约数
    const gcd = (a, b) => b === 0 ? a : gcd(b, a % b);
    const g = gcd(x, y);

    // 5. 判断 z 是否是 gcd 的倍数
    return z % g === 0;
};

⏱️ 复杂度分析

  • 时间复杂度:O(log(min(x, y))) —— 欧几里得算法效率极高
  • 空间复杂度:O(log(min(x, y)))(递归栈),可改写为迭代版本降至 O(1)

这是最优解法,适用于任意规模的数据!


🆚 五、两种方法全面对比

维度DFS 解法数学法(贝祖定理)
核心思想模拟操作,穷举所有状态抽象为线性方程,利用数论判断
时间复杂度O(X × Y)O(log(min(X, Y))) ≈ 常数级
空间复杂度O(X × Y)O(1) ~ O(log n)
适用范围小数据(x, y ≤ 1000)任意大小(包括 10910^9
理解难度直观易懂,适合初学者需了解贝祖定理,有一定数学门槛
能否输出路径可以回溯记录操作路径仅判断可行性,不提供具体操作步骤

🧠 六、关键结论与思维升华

✅ 关键结论总结

  1. 水壶问题的本质是构造线性组合
    所有操作等价于对总水量进行 ±x±y 的调整。

  2. 贝祖定理是突破口
    当且仅当 zgcd(x, y) 的倍数,并且不超过总容量时,才可构造成功。

  3. DFS 是理解工具,数学才是通解
    模拟法帮助建立直觉,但真正高效的解法往往来自抽象建模。


🌟 思维启示:如何提升算法能力?

“把实际问题转化为数学模型” 是高级程序员的核心竞争力。

本题就是一个绝佳范例:

  • 初看像是 BFS/DFS 的状态搜索题;
  • 深入分析后发现其背后隐藏着深刻的数论结构;
  • 掌握这种“去表象、抓本质”的能力,才能写出优雅高效的代码。

✅ 结语

LeetCode 365 不仅仅是一道面试题,它是一座桥梁,连接了模拟思维数学抽象

🎯 学习算法,不只是背模板,更是训练“将复杂问题简化为基本模型”的能力。

下次当你面对看似复杂的操作类问题时,不妨多问一句:

“这些操作的背后,是否隐藏着某种不变量?能否用数学语言重新表述?”

也许,答案就在那一瞬间豁然开朗。