引言
三壶谜题(Water Jug Problem)是经典的数学益智问题:给定两个容量分别为 m 升和 n 升的水壶,以及一个无限水源,如何通过倒满、倒空、互倒等操作,精确量出 d 升水?本文将用 Java 完整实现三壶谜题的求解系统,核心讲解广度优先搜索(BFS)遍历状态空间与贝祖定理(Bézout’s Identity)判定问题可解性两大算法。
问题建模与状态空间
状态表示
设两个水壶的当前水量分别为 (x, y),其中 0 ≤ x ≤ m,0 ≤ y ≤ n。每次操作后系统进入新状态,所有可达状态构成状态空间图。以 (3, 5) 量出 4 升为例:
| 操作 | 状态变化 |
|---|---|
| 倒满5升壶 | (0, 5) |
| 5升倒入3升壶 | (3, 2) |
| 倒空3升壶 | (0, 2) |
| 2升倒入3升壶 | (2, 0) |
| 倒满5升壶 | (2, 5) |
| 5升壶倒入3升壶至满 | (3, 4) |
此时5升壶中恰好剩余 4 升,问题得解。
可解性判定:贝祖定理
并非所有 (m, n, d) 组合都有解。根据贝祖定理:对于整数 m, n,方程 mx + ny = d 有整数解的充要条件是 gcd(m, n) | d。在三壶谜题中,由于操作只允许加减 m 或 n 的倍数,因此:
可解条件:
d ≤ max(m, n)且d % gcd(m, n) == 0
/**
* 贝祖定理判定三壶谜题是否可解
*/
public class JugSolver {
/**
* 计算最大公约数(辗转相除法)
*/
private static int gcd(int a, int b) {
while (b != 0) {
int temp = b;
b = a % b;
a = temp;
}
return a;
}
/**
* 判定问题是否可解
* @param m 第一个水壶容量
* @param n 第二个水壶容量
* @param d 目标水量
* @return 是否可达
*/
public static boolean isSolvable(int m, int n, int d) {
if (d < 0 || d > Math.max(m, n)) {
return false;
}
return d % gcd(m, n) == 0;
}
}
BFS 状态空间搜索
六种基本操作
从任意状态 (x, y) 出发,共有六种合法操作:
- 倒满第一个壶:
(m, y) - 倒满第二个壶:
(x, n) - 倒空第一个壶:
(0, y) - 倒空第二个壶:
(x, 0) - 第一个壶倒入第二个壶:
(x - pour, y + pour),其中pour = min(x, n - y) - 第二个壶倒入第一个壶:
(x + pour, y - pour),其中pour = min(y, m - x)
完整 Java 实现
import java.util.*;
/**
* 三壶谜题求解器:BFS + 贝祖定理
*/
public class WaterJugSolver {
// 记录每个状态的前驱状态与操作,用于路径回溯
static class StateInfo {
final int x, y;
final String action;
final StateInfo parent;
StateInfo(int x, int y, String action, StateInfo parent) {
this.x = x;
this.y = y;
this.action = action;
this.parent = parent;
}
}
/**
* BFS搜索最短操作序列
* @param m 壶1容量
* @param n 壶2容量
* @param d 目标水量
* @return 操作序列,无解返回null
*/
public static List<String> solve(int m, int n, int d) {
if (!isSolvable(m, n, d)) {
System.out.println("根据贝祖定理,该问题不可解");
return null;
}
// visited[i][j] 表示状态(i,j)是否已访问
boolean[][] visited = new boolean[m + 1][n + 1];
Queue<StateInfo> queue = new LinkedList<>();
queue.offer(new StateInfo(0, 0, "初始状态", null));
visited[0][0] = true;
while (!queue.isEmpty()) {
StateInfo curr = queue.poll();
int x = curr.x, y = curr.y;
// 检查是否达成目标:任一水壶中有d升水
if (x == d || y == d) {
return reconstructPath(curr);
}
// 生成六种操作的下一状态
List<StateInfo> nextStates = Arrays.asList(
new StateInfo(m, y, "倒满第一个壶", curr),
new StateInfo(x, n, "倒满第二个壶", curr),
new StateInfo(0, y, "倒空第一个壶", curr),
new StateInfo(x, 0, "倒空第二个壶", curr),
pour(x, y, m, n, true, curr), // 1 -> 2
pour(x, y, m, n, false, curr) // 2 -> 1
);
for (StateInfo next : nextStates) {
if (next == null) continue;
if (!visited[next.x][next.y]) {
visited[next.x][next.y] = true;
queue.offer(next);
}
}
}
return null; // 理论上不会到达,因为已通过贝祖定理判定
}
/**
* 执行倒水操作
* @param fromFirst true表示从壶1倒向壶2
*/
private static StateInfo pour(int x, int y, int m, int n,
boolean fromFirst, StateInfo parent) {
if (fromFirst) {
int pour = Math.min(x, n - y);
if (pour == 0) return null;
return new StateInfo(x - pour, y + pour,
String.format("第一个壶倒入第二个壶 %d 升", pour), parent);
} else {
int pour = Math.min(y, m - x);
if (pour == 0) return null;
return new StateInfo(x + pour, y - pour,
String.format("第二个壶倒入第一个壶 %d 升", pour), parent);
}
}
/**
* 回溯重构操作路径
*/
private static List<String> reconstructPath(StateInfo end) {
List<String> path = new ArrayList<>();
StateInfo curr = end;
while (curr != null) {
path.add(String.format("(%d, %d) —— %s", curr.x, curr.y, curr.action));
curr = curr.parent;
}
Collections.reverse(path);
return path;
}
private static boolean isSolvable(int m, int n, int d) {
if (d > Math.max(m, n)) return false;
int gcd = gcd(m, n);
return d % gcd == 0;
}
private static int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
public static void main(String[] args) {
int m = 3, n = 5, d = 4;
System.out.printf("求解:壶容量(%d, %d),目标 %d 升%n%n", m, n, d);
List<String> path = solve(m, n, d);
if (path != null) {
System.out.println("最短操作序列(共 " + (path.size() - 1) + " 步):");
for (String step : path) {
System.out.println(" " + step);
}
}
}
}
运行结果:
求解:壶容量(3, 5),目标 4 升
最短操作序列(共 6 步):
(0, 0) —— 初始状态
(0, 5) —— 倒满第二个壶
(3, 2) —— 第二个壶倒入第一个壶 3 升
(0, 2) —— 倒空第一个壶
(2, 0) —— 第二个壶倒入第一个壶 2 升
(2, 5) —— 倒满第二个壶
(3, 4) —— 第二个壶倒入第一个壶 1 升
复杂度分析
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(m × n) |
每个状态最多访问一次 |
| 空间复杂度 | O(m × n) |
visited数组与BFS队列 |
| 可解性判定 | O(log(min(m,n))) |
辗转相除求GCD |
总结
三壶谜题将数论与图搜索巧妙结合:贝祖定理在 O(log N) 时间内排除无解情况,BFS 在 O(m × n) 状态空间内保证找到最短操作序列。这种”先判定后求解”的策略是算法设计的经典范式,广泛应用于路径规划、资源调度等实际场景。