每日算法 — 使用java实现传教士与野人:状态空间BFS与安全性约束判定

传教士与野人问题是经典的AI搜索问题。3名传教士和3名野人要过河,只有一条船,船最多容纳2人。在任何岸边,如果野人数量多于传教士,传教士就会被吃掉。我们需要找到让所有6人安全过河的最少步数方案。

问题建模

将问题抽象为状态空间搜索:
– 状态用三元组 (M, C, B) 表示,其中 M 为左岸传教士数,C 为左岸野人数,B 为船位置(0左岸/1右岸)
– 初始状态:(3, 3, 0),目标状态:(0, 0, 1)
– 每次船移动,可以载 1~2 人(至少1人划船)
– 约束条件:任何岸边野人不得多于传教士(除非传教士为0)

核心算法:BFS状态空间搜索

使用BFS遍历所有合法状态,保证找到最短路径。每个状态记录前驱状态以便回溯路径。

import java.util.*;

/**
 * 传教士与野人问题求解器
 * 使用BFS状态空间搜索找到最短安全过河方案
 */
public class MissionariesAndCannibals {

    // 状态类,记录当前局面
    static class State {
        int m;      // 左岸传教士数
        int c;      // 左岸野人数
        int boat;   // 船位置:0=左岸,1=右岸
        State parent; // 前驱状态,用于回溯路径
        String action; // 本次移动描述

        State(int m, int c, int boat, State parent, String action) {
            this.m = m;
            this.c = c;
            this.boat = boat;
            this.parent = parent;
            this.action = action;
        }

        // 状态唯一标识,用于判重
        String key() {
            return m + "," + c + "," + boat;
        }
    }

    private final int totalM; // 传教士总数
    private final int totalC; // 野人总数
    private final int boatCap; // 船容量

    public MissionariesAndCannibals(int m, int c, int cap) {
        this.totalM = m;
        this.totalC = c;
        this.boatCap = cap;
    }

    /**
     * 检查状态是否安全:任何岸边野人不得多于传教士(传教士为0除外)
     */
    private boolean isValid(int leftM, int leftC) {
        int rightM = totalM - leftM;
        int rightC = totalC - leftC;
        // 左岸安全:传教士为0 或 传教士 >= 野人
        if (leftM > 0 && leftC > leftM) return false;
        // 右岸安全
        if (rightM > 0 && rightC > rightM) return false;
        return true;
    }

    /**
     * BFS搜索最短过河方案
     */
    public List<String> solve() {
        Queue<State> queue = new LinkedList<>();
        Set<String> visited = new HashSet<>();

        State start = new State(totalM, totalC, 0, null, "初始状态:全部在左岸");
        queue.offer(start);
        visited.add(start.key());

        // 船可行的载客方案:(传教士数, 野人数)
        int[][] moves = generateMoves();

        while (!queue.isEmpty()) {
            State cur = queue.poll();

            // 到达目标状态
            if (cur.m == 0 && cur.c == 0 && cur.boat == 1) {
                return buildPath(cur);
            }

            // 尝试所有可行的移动
            for (int[] move : moves) {
                int dm = move[0]; // 传教士移动数
                int dc = move[1]; // 野人移动数

                State next;
                if (cur.boat == 0) {
                    // 船在左岸,载人去右岸
                    next = new State(cur.m - dm, cur.c - dc, 1, cur,
                        String.format("%d传教士+%d野人 左->右", dm, dc));
                } else {
                    // 船在右岸,载人回左岸
                    next = new State(cur.m + dm, cur.c + dc, 0, cur,
                        String.format("%d传教士+%d野人 右->左", dm, dc));
                }

                // 边界检查与安全性检查
                if (next.m < 0 || next.m > totalM) continue;
                if (next.c < 0 || next.c > totalC) continue;
                if (!isValid(next.m, next.c)) continue;
                if (visited.contains(next.key())) continue;

                visited.add(next.key());
                queue.offer(next);
            }
        }

        return Collections.emptyList(); // 无解
    }

    /**
     * 生成所有可行的船上载客方案
     */
    private int[][] generateMoves() {
        List<int[]> list = new ArrayList<>();
        for (int dm = 0; dm <= boatCap; dm++) {
            for (int dc = 0; dc <= boatCap; dc++) {
                if (dm + dc >= 1 && dm + dc <= boatCap) {
                    list.add(new int[]{dm, dc});
                }
            }
        }
        return list.toArray(new int[0][]);
    }

    /**
     * 从目标状态回溯到起点,构建路径
     */
    private List<String> buildPath(State target) {
        LinkedList<String> path = new LinkedList<>();
        State cur = target;
        while (cur != null) {
            path.addFirst(String.format("[%d missionary, %d cannibal, boat=%s] -> %s",
                cur.m, cur.c, cur.boat == 0 ? "左" : "右", cur.action));
            cur = cur.parent;
        }
        return path;
    }

    public static void main(String[] args) {
        MissionariesAndCannibals solver = new MissionariesAndCannibals(3, 3, 2);
        List<String> solution = solver.solve();

        System.out.println("=== 传教士与野人问题求解 ===");
        if (solution.isEmpty()) {
            System.out.println("无解!");
        } else {
            System.out.println("最短方案需 " + (solution.size() - 1) + " 次渡河:");
            for (String step : solution) {
                System.out.println(step);
            }
        }
    }
}

复杂度分析

  • 时间复杂度:O(S),其中 S 为合法状态总数。对于 (M, C, B) 三元组,最多 (M+1)(C+1)*2 个状态。本例中最多 4*4*2 = 32 个状态,BFS在极短时间内完成。
  • 空间复杂度:O(S),用于存储已访问状态集合和BFS队列。
  • 状态剪枝:通过安全性约束(两岸传教士不得被野人超过)和访问判重,有效剪除大量非法状态。

扩展思考

  • 若传教士与野人数量不等,或船容量变化,只需调整参数即可复用算法。
  • 可改用A*搜索,设计启发函数(如两岸总人数/船容量)加速搜索。
  • 状态空间可扩展至更多经典过河问题,如狼羊菜问题。

总结

本文通过BFS状态空间搜索解决传教士与野人问题,展示了如何将逻辑谜题转化为图搜索问题。核心在于正确的状态编码、合法性约束判定以及路径回溯机制。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注