每日算法 — 使用java实现过河问题:BFS状态空间搜索与安全性约束判定

问题描述

过河问题是经典的算法教学案例:一位农夫需要带着狼、羊和白菜过河。船上只能容纳农夫和一件物品。如果农夫不在场,狼会吃羊,羊会吃白菜。如何设计一个安全的过河方案?

本文将这一问题抽象为状态空间搜索问题,使用BFS(广度优先搜索)寻找最短可行解,并通过安全性约束判定确保每一步都不会导致物品被吃掉。

核心算法思路

状态表示

用四位二进制数表示状态,每一位分别代表农夫、狼、羊、白菜是否在起始岸(1)或对岸(0)。例如状态 1111 表示所有角色都在起始岸,0000 表示全部到达对岸。

安全性约束

一个状态是安全的,当且仅当不满足以下任一条件:
– 狼和羊在同一岸,且农夫不在该岸
– 羊和白菜在同一岸,且农夫不在该岸

BFS搜索

从初始状态 1111 出发,枚举农夫可以携带的物品(不携带、狼、羊、白菜),生成所有合法的新状态,使用BFS逐层扩展直到到达目标状态 0000。记录每个状态的前驱状态以重构路径。

Java实现

import java.util.*;

/**
 * 过河问题求解器
 * 使用BFS状态空间搜索寻找最短安全过河方案
 */
public class RiverCrossingSolver {

    // 状态位定义:农夫(3)、狼(2)、羊(1)、白菜(0)
    private static final int FARMER = 3;
    private static final int WOLF = 2;
    private static final int SHEEP = 1;
    private static final int CABBAGE = 0;

    // 角色名称映射
    private static final String[] NAMES = {"白菜", "羊", "狼", "农夫"};

    /**
     * 判断当前状态是否安全
     * @param state 4位二进制状态
     * @return 安全返回true
     */
    public boolean isSafe(int state) {
        boolean farmerHere = ((state >> FARMER) & 1) == 1;
        boolean wolfHere = ((state >> WOLF) & 1) == 1;
        boolean sheepHere = ((state >> SHEEP) & 1) == 1;
        boolean cabbageHere = ((state >> CABBAGE) & 1) == 1;

        // 农夫不在此岸时,检查危险组合
        if (!farmerHere) {
            // 狼和羊单独在一起
            if (wolfHere && sheepHere) return false;
            // 羊和白菜单独在一起
            if (sheepHere && cabbageHere) return false;
        }

        // 检查对岸(农夫不在对岸时)
        boolean farmerThere = !farmerHere;
        boolean wolfThere = !wolfHere;
        boolean sheepThere = !sheepHere;
        boolean cabbageThere = !cabbageHere;

        if (!farmerThere) {
            if (wolfThere && sheepThere) return false;
            if (sheepThere && cabbageThere) return false;
        }

        return true;
    }

    /**
     * 获取农夫可以携带的物品列表
     * @param state 当前状态
     * @return 可携带物品的位掩码列表
     */
    public List<Integer> getPossibleMoves(int state) {
        List<Integer> moves = new ArrayList<>();
        boolean farmerHere = ((state >> FARMER) & 1) == 1;

        // 农夫可以单独过河
        moves.add(0);

        // 检查其他角色是否与农夫同岸
        for (int item = 0; item <= 2; item++) {
            boolean itemHere = ((state >> item) & 1) == 1;
            if (itemHere == farmerHere) {
                moves.add(1 << item);
            }
        }

        return moves;
    }

    /**
     * 执行状态转移
     * @param state 当前状态
     * @param move 携带的物品掩码
     * @return 新状态
     */
    public int applyMove(int state, int move) {
        // 农夫和携带的物品都翻转状态
        int newState = state ^ (1 << FARMER);
        if (move != 0) {
            // 找到move中唯一的1的位
            for (int i = 0; i <= 2; i++) {
                if ((move & (1 << i)) != 0) {
                    newState ^= (1 << i);
                    break;
                }
            }
        }
        return newState;
    }

    /**
     * BFS搜索最短过河方案
     * @return 状态转移路径,无解返回空列表
     */
    public List<int[]> solve() {
        int start = 0b1111; // 15
        int goal = 0b0000;  // 0

        if (!isSafe(start)) return new ArrayList<>();

        Queue<Integer> queue = new LinkedList<>();
        Map<Integer, Integer> parent = new HashMap<>();
        Map<Integer, Integer> parentMove = new HashMap<>();
        Set<Integer> visited = new HashSet<>();

        queue.offer(start);
        visited.add(start);
        parent.put(start, -1);
        parentMove.put(start, -1);

        while (!queue.isEmpty()) {
            int current = queue.poll();

            if (current == goal) {
                return reconstructPath(parent, parentMove, current);
            }

            for (int move : getPossibleMoves(current)) {
                int next = applyMove(current, move);

                if (!visited.contains(next) && isSafe(next)) {
                    visited.add(next);
                    parent.put(next, current);
                    parentMove.put(next, move);
                    queue.offer(next);
                }
            }
        }

        return new ArrayList<>(); // 无解
    }

    /**
     * 重构路径
     */
    private List<int[]> reconstructPath(Map<Integer, Integer> parent,
                                         Map<Integer, Integer> parentMove,
                                         int goal) {
        List<int[]> path = new ArrayList<>();
        int current = goal;

        while (current != -1) {
            int move = parentMove.getOrDefault(current, -1);
            path.add(new int[]{current, move});
            current = parent.getOrDefault(current, -1);
        }

        Collections.reverse(path);
        return path;
    }

    /**
     * 打印路径
     */
    public void printSolution(List<int[]> path) {
        if (path.isEmpty()) {
            System.out.println("无解!");
            return;
        }

        System.out.println("=== 过河方案 ===");
        for (int i = 0; i < path.size(); i++) {
            int[] step = path.get(i);
            int state = step[0];
            int move = step[1];

            System.out.printf("步骤 %d: ", i);
            printState(state);

            if (i < path.size() - 1) {
                System.out.print("  -> 携带: ");
                if (move == 0) {
                    System.out.println("农夫独自过河");
                } else {
                    for (int j = 0; j <= 2; j++) {
                        if ((move & (1 << j)) != 0) {
                            System.out.println(NAMES[j]);
                            break;
                        }
                    }
                }
            }
        }
        System.out.println("\n总共需要 " + (path.size() - 1) + " 次渡河");
    }

    private void printState(int state) {
        StringBuilder here = new StringBuilder("起始岸: [");
        StringBuilder there = new StringBuilder("对岸: [");

        for (int i = 3; i >= 0; i--) {
            boolean isHere = ((state >> i) & 1) == 1;
            if (isHere) {
                if (here.length() > 5) here.append(", ");
                here.append(NAMES[i]);
            } else {
                if (there.length() > 4) there.append(", ");
                there.append(NAMES[i]);
            }
        }

        here.append("]");
        there.append("]");
        System.out.println(here + " | " + there);
    }

    public static void main(String[] args) {
        RiverCrossingSolver solver = new RiverCrossingSolver();
        List<int[]> path = solver.solve();
        solver.printSolution(path);
    }
}

算法复杂度分析

  • 时间复杂度:$O(2^n)$,其中 $n=4$ 为角色数量。实际搜索空间远小于 $2^4=16$,因为安全性约束剪枝了大量非法状态。
  • 空间复杂度:$O(2^n)$,用于存储访问标记和前驱状态。

扩展思考

  1. 通用化:若有 $n$ 个角色和 $m$ 组危险关系,可将问题建模为图论中的独立集问题,状态空间会指数级增长。
  2. 最优性:BFS保证找到步数最少的解。若需要最小化运输成本,可将BFS替换为Dijkstra算法。
  3. 双向BFS:对于更大规模的状态空间,可从起始状态和目标状态同时搜索,降低时间复杂度。

总结

过河问题将现实场景抽象为状态空间搜索模型,通过BFS系统性地探索所有合法状态转移,安全性约束作为剪枝条件有效缩小了搜索范围。这种建模方法可广泛应用于路径规划、调度优化和博弈求解等领域。