🧩 一、题目回顾:水壶问题到底在问什么?
题目链接:LeetCode 365. 水壶问题
✅ 题意描述
💡 二、核心洞察:操作的本质是什么?
我们来思考每一次操作对“总水量”的影响:
| 操作 | 对总水量的影响 |
|---|---|
| 装满 X | +X |
| 装满 Y | +Y |
| 清空 X | -X |
| 清空 Y | -Y |
| 倒水(X→Y 或 Y→X) | 总量不变(只是转移) |
👉 所以,只有装满和清空会改变总水量,而倒水只是内部调配。
因此,整个过程的总水量始终是形如:
其中 是整数(正表示加水,负表示倒掉)。最终我们要判断是否存在整数 ,使得:
这正是经典的线性不定方程求解问题!
🌲 三、思路一:深度优先搜索(DFS)——暴力穷举所有状态
✅ 核心思想
既然无法直接看出数学规律,我们可以采用最直观的方式:枚举所有可能的状态组合。
每个状态由 (remainX, remainY) 表示,即当前两个水壶中的水量。
- 初始状态:
(0, 0) - 目标状态:满足
remainX == z或remainY == z或remainX + 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),用于存储已访问状态
❗ 局限性
当 x 和 y 很大时(比如 ),状态空间爆炸,程序会超时或内存溢出。
所以这个方法只适用于小数据场景。
📐 四、思路二:数学方法——贝祖定理(Bézout's Identity)
✅ 核心理论:贝祖定理
对于整数 、,存在整数 、 使得:
更一般地,方程 有整数解的充要条件是:
换句话说:只要 z 是 gcd(x, y) 的倍数,就可以通过若干次“加减 x”和“加减 y”构造出来。
但这还不够!我们必须结合实际物理限制。
🔒 加入边界条件:现实世界的约束
即使数学上可行,也要考虑水壶的实际容量限制:
-
总水量不能超过两壶容量之和
→ 若z > x + y,一定不可能实现。 -
特殊情况处理:x 或 y 为 0
- 如果
x === 0 && y === 0,则只能得到 0 升水; - 如果其中一个为 0,则只能得到 0 或非零的那个容量。
- 如果
-
z === 0 的情况总是成立(什么都不做即可)
✅ 最终判断逻辑
综合以上分析,答案成立的充要条件是:
(z <= x + y) && (z % gcd(x, y) === 0)
再加上特判 x 或 y 为 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) | 任意大小(包括 ) |
| 理解难度 | 直观易懂,适合初学者 | 需了解贝祖定理,有一定数学门槛 |
| 能否输出路径 | 可以回溯记录操作路径 | 仅判断可行性,不提供具体操作步骤 |
🧠 六、关键结论与思维升华
✅ 关键结论总结
-
水壶问题的本质是构造线性组合:
所有操作等价于对总水量进行±x和±y的调整。 -
贝祖定理是突破口:
当且仅当z是gcd(x, y)的倍数,并且不超过总容量时,才可构造成功。 -
DFS 是理解工具,数学才是通解:
模拟法帮助建立直觉,但真正高效的解法往往来自抽象建模。
🌟 思维启示:如何提升算法能力?
“把实际问题转化为数学模型” 是高级程序员的核心竞争力。
本题就是一个绝佳范例:
- 初看像是 BFS/DFS 的状态搜索题;
- 深入分析后发现其背后隐藏着深刻的数论结构;
- 掌握这种“去表象、抓本质”的能力,才能写出优雅高效的代码。
✅ 结语
LeetCode 365 不仅仅是一道面试题,它是一座桥梁,连接了模拟思维与数学抽象。
🎯 学习算法,不只是背模板,更是训练“将复杂问题简化为基本模型”的能力。
下次当你面对看似复杂的操作类问题时,不妨多问一句:
“这些操作的背后,是否隐藏着某种不变量?能否用数学语言重新表述?”
也许,答案就在那一瞬间豁然开朗。