引言
“狼羊菜”过河问题是中国古代著名的智力题:一位农夫需要把狼、羊和白菜运到河对岸,但船每次只能载农夫和一样物品。如果农夫不在场,狼会吃羊,羊会吃菜。如何用最少次数将所有物品安全运过河?本文将用Java实现这个经典问题的完整求解,深入讲解状态空间建模、BFS最短路径搜索,以及约束满足问题的通用解法框架。
一、问题建模
1.1 状态表示
用四位二进制数表示系统状态,每一位代表一个对象所在河岸(0=左岸,1=右岸):
– 第0位:农夫
– 第1位:狼
– 第2位:羊
– 第3位:菜
例如:状态0101(二进制)=5(十进制)表示农夫和羊在右岸,狼和菜在左岸。
1.2 约束条件
- 船容量限制:农夫每次只能带0或1个物品过河
- 安全约束:
- 狼和羊不能独处(无农夫时不能同岸)
- 羊和菜不能独处(无农夫时不能同岸)
1.3 状态转移
农夫可以单独过河,或带狼/羊/菜之一过河。每次转移后检查是否满足安全约束。
二、Java核心实现
import java.util.*;
/**
* 过河问题状态类
* 使用位运算高效表示和操作状态
*/
public class RiverCrossingState {
// 位掩码定义
public static final int FARMER = 1 << 0; // 0001 = 1
public static final int WOLF = 1 << 1; // 0010 = 2
public static final int SHEEP = 1 << 2; // 0100 = 4
public static final int CABBAGE = 1 << 3; // 1000 = 8
public static final int ALL_ITEMS = FARMER | WOLF | SHEEP | CABBAGE; // 1111 = 15
public static final int START_STATE = 0; // 全部在左岸
public static final int GOAL_STATE = ALL_ITEMS; // 全部在右岸
private final int state; // 当前状态值
public RiverCrossingState(int state) {
this.state = state;
}
/**
* 检查指定对象是否在右岸
*/
public boolean isOnRightBank(int item) {
return (state & item) != 0;
}
/**
* 检查指定对象是否在左岸
*/
public boolean isOnLeftBank(int item) {
return (state & item) == 0;
}
/**
* 获取当前状态值
*/
public int getValue() {
return state;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof RiverCrossingState)) return false;
return state == ((RiverCrossingState) o).state;
}
@Override
public int hashCode() {
return state;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("State(").append(state).append("): ");
sb.append("左岸[");
if (isOnLeftBank(FARMER)) sb.append("农");
if (isOnLeftBank(WOLF)) sb.append("狼");
if (isOnLeftBank(SHEEP)) sb.append("羊");
if (isOnLeftBank(CABBAGE)) sb.append("菜");
sb.append("] 右岸[");
if (isOnRightBank(FARMER)) sb.append("农");
if (isOnRightBank(WOLF)) sb.append("狼");
if (isOnRightBank(SHEEP)) sb.append("羊");
if (isOnRightBank(CABBAGE)) sb.append("菜");
sb.append("]");
return sb.toString();
}
/**
* 获取可携带的物品掩码(与农夫同岸的物品)
*/
public int getAvailableItems() {
int available = 0;
if (isOnRightBank(FARMER)) {
if (isOnRightBank(WOLF)) available |= WOLF;
if (isOnRightBank(SHEEP)) available |= SHEEP;
if (isOnRightBank(CABBAGE)) available |= CABBAGE;
} else {
if (isOnLeftBank(WOLF)) available |= WOLF;
if (isOnLeftBank(SHEEP)) available |= SHEEP;
if (isOnLeftBank(CABBAGE)) available |= CABBAGE;
}
return available;
}
}
/**
* 过河问题求解器
* 使用BFS找到最少次数的解决方案
*/
public class RiverCrossingSolver {
/**
* 检查状态是否安全(没有物品被吃掉)
* 不安全条件:
* 1. 农夫不在左岸,且狼和羊都在左岸
* 2. 农夫不在左岸,且羊和菜都在左岸
* 3. 农夫不在右岸,且狼和羊都在右岸
* 4. 农夫不在右岸,且羊和菜都在右岸
*/
public static boolean isSafe(int state) {
boolean farmerLeft = (state & RiverCrossingState.FARMER) == 0;
boolean wolfLeft = (state & RiverCrossingState.WOLF) == 0;
boolean sheepLeft = (state & RiverCrossingState.SHEEP) == 0;
boolean cabbageLeft = (state & RiverCrossingState.CABBAGE) == 0;
// 检查左岸安全性
if (farmerLeft) {
// 农夫在左岸,左岸安全
// 检查右岸:狼羊不能独处,羊菜不能独处
if (!wolfLeft && !sheepLeft && cabbageLeft) return false; // 右岸狼+羊
if (!sheepLeft && !cabbageLeft && wolfLeft) return false; // 右岸羊+菜
} else {
// 农夫在右岸,右岸安全
// 检查左岸
if (wolfLeft && sheepLeft && !cabbageLeft) return false; // 左岸狼+羊
if (sheepLeft && cabbageLeft && !wolfLeft) return false; // 左岸羊+菜
}
return true;
}
/**
* 使用BFS求解最少过河次数
* @return 包含完整状态转移路径的列表,若无解则返回null
*/
public static List<RiverCrossingState> solveBfs() {
Queue<Integer> queue = new LinkedList<>();
Map<Integer, Integer> parent = new HashMap<>(); // 记录每个状态的前驱
Map<Integer, Integer> action = new HashMap<>(); // 记录到达该状态的操作
Set<Integer> visited = new HashSet<>();
int start = RiverCrossingState.START_STATE;
int goal = RiverCrossingState.GOAL_STATE;
queue.add(start);
visited.add(start);
parent.put(start, null);
while (!queue.isEmpty()) {
int current = queue.poll();
if (current == goal) {
return reconstructPath(parent, start, goal);
}
// 生成所有可能的下一步状态
List<Integer> nextStates = generateNextStates(current);
for (int next : nextStates) {
if (!visited.contains(next) && isSafe(next)) {
visited.add(next);
parent.put(next, current);
queue.add(next);
}
}
}
return null; // 无解
}
/**
* 生成当前状态的所有合法后继状态
*/
private static List<Integer> generateNextStates(int current) {
List<Integer> nextStates = new ArrayList<>();
boolean farmerRight = (current & RiverCrossingState.FARMER) != 0;
// 农夫单独过河
int next = current ^ RiverCrossingState.FARMER;
nextStates.add(next);
// 农夫带一个物品过河(只能带同岸的物品)
int[] items = {RiverCrossingState.WOLF, RiverCrossingState.SHEEP, RiverCrossingState.CABBAGE};
for (int item : items) {
// 检查物品是否与农夫同岸
boolean itemSameSide = ((current & RiverCrossingState.FARMER) != 0) ==
((current & item) != 0);
if (itemSameSide) {
// 农夫和该物品一起过河
next = current ^ RiverCrossingState.FARMER ^ item;
nextStates.add(next);
}
}
return nextStates;
}
/**
* 回溯重建路径
*/
private static List<RiverCrossingState> reconstructPath(
Map<Integer, Integer> parent, int start, int goal) {
List<RiverCrossingState> path = new ArrayList<>();
Integer current = goal;
while (current != null) {
path.add(new RiverCrossingState(current));
current = parent.get(current);
}
Collections.reverse(path);
return path;
}
/**
* 打印带操作的解决方案
*/
public static void printSolution(List<RiverCrossingState> path) {
if (path == null) {
System.out.println("无解!");
return;
}
System.out.println("===== 最少次数解决方案 =====");
System.out.println("总共需要 " + (path.size() - 1) + " 次渡河\n");
for (int i = 0; i < path.size(); i++) {
System.out.println("步骤 " + i + ": " + path.get(i));
if (i < path.size() - 1) {
String action = describeAction(path.get(i), path.get(i + 1));
System.out.println(" 操作: " + action);
System.out.println();
}
}
}
/**
* 描述从状态a到状态b的操作
*/
private static String describeAction(RiverCrossingState from, RiverCrossingState to) {
int diff = from.getValue() ^ to.getValue();
boolean goingRight = !from.isOnRightBank(RiverCrossingState.FARMER);
String direction = goingRight ? "从左岸到右岸" : "从右岸到左岸";
StringBuilder carried = new StringBuilder();
if ((diff & RiverCrossingState.WOLF) != 0) carried.append("狼");
if ((diff & RiverCrossingState.SHEEP) != 0) carried.append("羊");
if ((diff & RiverCrossingState.CABBAGE) != 0) carried.append("菜");
if (carried.length() == 0) {
return "农夫独自渡河 " + direction;
} else {
return "农夫带" + carried + "渡河 " + direction;
}
}
/**
* 查找所有可能的解决方案(不只是最短路径)
*/
public static List<List<RiverCrossingState>> findAllSolutions(int maxDepth) {
List<List<RiverCrossingState>> allSolutions = new ArrayList<>();
Set<Integer> visited = new HashSet<>();
List<RiverCrossingState> currentPath = new ArrayList<>();
dfsFindAll(RiverCrossingState.START_STATE, RiverCrossingState.GOAL_STATE,
visited, currentPath, allSolutions, maxDepth, 0);
return allSolutions;
}
private static void dfsFindAll(int current, int goal, Set<Integer> visited,
List<RiverCrossingState> path,
List<List<RiverCrossingState>> solutions,
int maxDepth, int depth) {
if (depth > maxDepth) return;
path.add(new RiverCrossingState(current));
if (current == goal) {
solutions.add(new ArrayList<>(path));
path.remove(path.size() - 1);
return;
}
visited.add(current);
List<Integer> nextStates = generateNextStates(current);
for (int next : nextStates) {
if (!visited.contains(next) && isSafe(next)) {
dfsFindAll(next, goal, visited, path, solutions, maxDepth, depth + 1);
}
}
visited.remove(current);
path.remove(path.size() - 1);
}
}
三、通用约束满足框架
/**
* 通用过河问题求解器
* 可以处理任意数量的角色和约束
*/
public class GenericRiverSolver {
/**
* 约束接口:定义什么样的状态是安全的
*/
interface Constraint {
boolean isSafe(int state, int numItems);
}
/**
* 经典的狼羊菜约束
*/
static class ClassicConstraint implements Constraint {
@Override
public boolean isSafe(int state, int numItems) {
// 狼(1)和羊(2)不能独处
// 羊(2)和菜(3)不能独处
boolean farmerRight = (state & 1) != 0;
for (int bank = 0; bank <= 1; bank++) {
boolean farmerHere = farmerRight == (bank == 1);
if (farmerHere) continue; // 农夫在该岸,安全
boolean wolfHere = ((state & 2) != 0) == (bank == 1);
boolean sheepHere = ((state & 4) != 0) == (bank == 1);
boolean cabbageHere = ((state & 8) != 0) == (bank == 1);
if (wolfHere && sheepHere && !cabbageHere) return false;
if (sheepHere && cabbageHere && !wolfHere) return false;
}
return true;
}
}
/**
* 传教士与食人族约束
* 3传教士+3食人族,船载2人
* 任何岸上食人族不能多于传教士(除非传教士为0)
*/
static class MissionariesConstraint implements Constraint {
@Override
public boolean isSafe(int state, int numItems) {
// 状态编码:低3位=右岸传教士(0-3),中3位=右岸食人族(0-3)
int missionariesRight = state & 7;
int cannibalsRight = (state >> 3) & 7;
int missionariesLeft = 3 - missionariesRight;
int cannibalsLeft = 3 - cannibalsRight;
// 检查两岸安全
if (missionariesLeft > 0 && cannibalsLeft > missionariesLeft) return false;
if (missionariesRight > 0 && cannibalsRight > missionariesRight) return false;
return true;
}
}
/**
* 通用BFS求解
*/
public static List<Integer> solveGeneric(int numItems, int boatCapacity,
Constraint constraint, int goalState) {
Queue<Integer> queue = new LinkedList<>();
Map<Integer, Integer> parent = new HashMap<>();
Set<Integer> visited = new HashSet<>();
queue.add(0);
visited.add(0);
while (!queue.isEmpty()) {
int current = queue.poll();
if (current == goalState) {
return reconstructPath(parent, 0, goalState);
}
// 生成所有合法操作
List<Integer> moves = generateGenericMoves(current, numItems, boatCapacity);
for (int next : moves) {
if (!visited.contains(next) && constraint.isSafe(next, numItems)) {
visited.add(next);
parent.put(next, current);
queue.add(next);
}
}
}
return null;
}
/**
* 生成通用移动
*/
private static List<Integer> generateGenericMoves(int current, int numItems, int capacity) {
List<Integer> moves = new ArrayList<>();
boolean farmerRight = (current & 1) != 0;
// 农夫必须过河
int baseMove = current ^ 1;
moves.add(baseMove); // 单独过河
// 生成带0到capacity-1个物品的组合
int[] items = new int[numItems - 1];
for (int i = 0; i < numItems - 1; i++) {
items[i] = 1 << (i + 1);
}
// 简化为只考虑带1个物品的情况(经典问题)
for (int item : items) {
boolean sameSide = ((current & 1) != 0) == ((current & item) != 0);
if (sameSide) {
moves.add(current ^ 1 ^ item);
}
}
return moves;
}
private static List<Integer> reconstructPath(Map<Integer, Integer> parent,
int start, int goal) {
List<Integer> path = new ArrayList<>();
Integer current = goal;
while (current != null) {
path.add(current);
current = parent.get(current);
}
Collections.reverse(path);
return path;
}
}
四、完整运行示例
/**
* 狼羊菜过河问题主程序
*/
public class WolfSheepCabbageApp {
public static void main(String[] args) {
System.out.println("===== 狼羊菜过河问题 =====\n");
// BFS求解
List<RiverCrossingState> solution = RiverCrossingSolver.solveBfs();
RiverCrossingSolver.printSolution(solution);
// 查找所有解法
System.out.println("\n===== 查找所有解法(深度限制10) =====");
List<List<RiverCrossingState>> allSolutions =
RiverCrossingSolver.findAllSolutions(10);
System.out.println("找到 " + allSolutions.size() + " 种解法");
if (!allSolutions.isEmpty()) {
System.out.println("最短解法步数: " + (allSolutions.get(0).size() - 1));
}
// 可视化状态空间图
System.out.println("\n===== 状态空间分析 =====");
analyzeStateSpace();
// 传教士与食人族问题
System.out.println("\n===== 传教士与食人族问题 =====");
solveMissionaries();
}
private static void analyzeStateSpace() {
int totalStates = 16; // 2^4
int safeStates = 0;
int reachableStates = 0;
System.out.println("所有可能状态分析:");
for (int s = 0; s < totalStates; s++) {
boolean safe = RiverCrossingSolver.isSafe(s);
if (safe) safeStates++;
RiverCrossingState state = new RiverCrossingState(s);
System.out.println(" " + state + " -> " + (safe ? "安全" : "不安全"));
}
System.out.println("\n统计:");
System.out.println(" 总状态数: " + totalStates);
System.out.println(" 安全状态数: " + safeStates);
System.out.println(" 不安全状态数: " + (totalStates - safeStates));
}
private static void solveMissionaries() {
// 3传教士+3食人族,状态编码:右岸传教士(0-3) + 右岸食人族(0-3) * 8
GenericRiverSolver.Constraint constraint = new GenericRiverSolver.MissionariesConstraint();
List<Integer> path = GenericRiverSolver.solveGeneric(4, 2, constraint, 0x3F); // 111111 = 全部到右岸
if (path != null) {
System.out.println("找到解决方案!步数: " + (path.size() - 1));
for (int i = 0; i < path.size(); i++) {
int state = path.get(i);
int m = state & 7;
int c = (state >> 3) & 7;
System.out.println("步骤 " + i + ": 右岸(" + m + "传," + c + "食), 左岸(" +
(3-m) + "传," + (3-c) + "食)");
}
} else {
System.out.println("无解!");
}
}
}
五、算法分析
| 指标 | 狼羊菜问题 | 传教士食人族 |
|---|---|---|
| 状态空间 | 16 (2^4) | 64 (部分合法) |
| BFS时间 | O(16) | O(64) |
| BFS空间 | O(16) | O(64) |
| 最短解长度 | 7步 | 11步 |
| 总解法数 | 2种 | 多种 |
六、总结与扩展
本文通过位运算高效实现了经典过河问题的状态表示和BFS求解,并提供了通用约束满足框架。位运算使状态操作的时间复杂度降至O(1),非常适合小规模状态空间问题。
扩展方向:
1. 更多角色:增加狐狸、鸡、米等更多角色和约束
2. 多船问题:多条船同时渡河
3. 桥问题:多人在夜间过桥,每次最多两人,需手电筒
4. 图论建模:将状态空间显式建图后用Dijkstra求最短路径