当前位置: 首页 > article >正文

算法系列之广度优先搜索解决妖怪和尚过河问题

_20250308_234624.png

在算法学习中,广度优先搜索(BFS)是一种常用的图搜索算法,适用于解决最短路径问题、状态转换问题等。本文将介绍如何利用广度优先搜索解决经典的“妖怪和尚过河问题”。

问题描述

有三个妖怪和三个和尚需要过河。他们只有一条小船,且船最多只能载两人。在任何时候,无论是岸边还是船上,如果妖怪的数量多于和尚,和尚就会被吃掉。如何安排过河顺序,才能让所有妖怪和和尚安全过河?

_20250308_232541.png

问题分析

首先,我们需要将建立状态和动作的数学模型,我们要明确问题的状态表示。我们用五个属性来表示状态,

//左岸和尚数量
int leftMonk;
//左岸妖怪数量
int leftMonster;
//右岸和尚数量
int rightMonk;
//右岸妖怪数量
int rightMonster;
//-1代表左岸,1代表右岸
int boatLocation;

结合船移动的方向,一共右10中过河动作可供选择,分别是

* 1、一个妖怪从左岸到右岸
* 2、一个和尚从左岸到右岸
* 3、两个妖怪从左岸到右岸
* 4、两个和尚从左岸到右岸
* 5、一个和尚一个妖怪从左岸到右岸
* 6、一个妖怪从右岸到左岸
* 7、一个和尚从右岸到左岸
* 8、两个妖怪从右岸到左岸
* 9、两个和尚从右岸到左岸
* 10、一个和尚一个妖怪从右岸到左岸

我们的目标是从初始状态 (3, 3, 0, 0, -1) 通过一系列合法的移动,达到目标状态 (0, 0, 3, 3, 1)。

Java使用BFS解决问题

BFS 是一种逐层扩展的搜索算法,适用于寻找最短路径。我们可以将每个状态看作图中的一个节点,合法的移动就是节点之间的边。通过 BFS,我们可以找到从初始状态到目标状态的最短路径。

算法步骤

  1. 初始化:将初始状态 (3, 3, 0, 0, -1) 加入队列,并标记为已访问。

  2. 循环处理队列:

  • 从队列中取出一个状态。

  • 生成所有可能的下一步状态。

  • 检查这些状态是否合法(即不违反妖怪和和尚的数量限制)。

  • 如果某个状态是目标状态,则打印路径并返回。

  • 否则,将合法的未访问状态加入队列,并标记为已访问。

  1. 重复步骤2,直到队列为空或找到目标状态。

代码实现如下:

/**
 * 妖怪与和尚过河问题
 * 描述:
 * 有三个和尚和三个妖怪在河的左岸,他们需要过河到右岸。
 *
 * 只有一条小船,最多可以承载两个人。
 *
 * 在任何时候,无论是左岸还是右岸,如果和尚的数量少于妖怪的数量,和尚就会被妖怪吃掉。
 */
public class crossRiver {

    /**
     * 状态类(数据模型)
     */
    public static class State{
        //左岸和尚数量
        int leftMonk;
        //左岸妖怪数量
        int leftMonster;
        //右岸和尚数量
        int rightMonk;
        //右岸妖怪数量
        int rightMonster;
        //-1代表左岸,1代表右岸
        int boatLocation;
        //前一状态,用于回溯打印路径
        State preState;

        public State(int leftMonk, int leftMonster, int rightMonk, int rightMonster, int boatLocation, State preState) {
            this.leftMonk = leftMonk;
            this.leftMonster = leftMonster;
            this.rightMonk = rightMonk;
            this.rightMonster = rightMonster;
            this.boatLocation = boatLocation;
            this.preState = preState;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            State state = (State) o;
            return leftMonk == state.leftMonk && leftMonster == state.leftMonster && rightMonk == state.rightMonk && rightMonster == state.rightMonster && boatLocation == state.boatLocation ;
        }

        @Override
        public int hashCode() {
            return Objects.hash(leftMonk, leftMonster, rightMonk, rightMonster, boatLocation);
        }
    }

    /**
     * 动作类,记录妖怪与和尚过河的动作
     */
    @Data
    public static class Action{
        int monkNum; //船上和尚数量
        int monsterNum;//船上妖怪数量
        int boatLocation;//船移动后位置

        public Action(int monkNum, int monsterNum, int boatLocation) {
            this.monkNum = monkNum;
            this.boatLocation = boatLocation;
            this.monsterNum = monsterNum;
        }
    }


    /**
     * 列举过河动作的可供选择
     * 共有十种情况
     * 1、一个妖怪从左岸到右岸
     * 2、一个和尚从左岸到右岸
     * 3、两个妖怪从左岸到右岸
     * 4、两个和尚从左岸到右岸
     * 5、一个和尚一个妖怪从左岸到右岸
     * 6、一个妖怪从右岸到左岸
     * 7、一个和尚从右岸到左岸
     * 8、两个妖怪从右岸到左岸
     * 9、两个和尚从右岸到左岸
     * 10、一个和尚一个妖怪从右岸到左岸
     *
     */
    public static List<Action> getActions(){
        List<Action> actions = new ArrayList<>();
        //一个妖怪从左岸到右岸
        actions.add(new Action(0,1,1));
        //两个妖怪从左岸到右岸
        actions.add(new Action(0,2,1));
        //一个和尚从左岸到右岸
        actions.add(new Action(1,0,1));
        //两个和尚从左岸到右岸
        actions.add(new Action(2,0,1));
        //一个和尚一个妖怪从左岸到右岸
        actions.add(new Action(1,1,1));
        //一个妖怪从右岸到左岸
        actions.add(new Action(0,1,-1));
        //两个妖怪从右岸到左岸
        actions.add(new Action(0,2,-1));
        //一个和尚从右岸到左岸
        actions.add(new Action(1,0,-1));
        //两个和尚从右岸到左岸
        actions.add(new Action(2,0,-1));
        //一个和尚一个妖怪从右岸到左岸
        actions.add(new Action(1,1,-1));
        return actions;

    }

    /**
     * 初始状态
     */
    public static State initState(){
        State state = new State(3,3,0,0,-1,null);
        return state;
    }

    /**
     * 生成所有可能的下一个状态
     */
    public static List<State> generateNextStates(State state){
        List<State> nextStates = new ArrayList<>();
        State nextState;
        for (Action action : getActions()) {
            if(state.boatLocation != action.boatLocation){
                nextState = new State(state.leftMonk - action.monkNum*action.boatLocation, state.leftMonster - action.monsterNum*action.boatLocation,
                        state.rightMonk + action.monkNum*action.boatLocation, state.rightMonster + action.monsterNum*action.boatLocation,
                        action.boatLocation, state);
                //有效则添加
                if(checkState(nextState)){
                    nextStates.add(nextState);
                }
            }
        }
        return nextStates;
    }

    /**
     * 检查状态是否有效,(无论是左岸还是右岸,和尚数量大于妖怪数量则有效)
     */
    public static boolean checkState(State state) {
        //任何一岸的和尚数量不能少于妖怪数量,除非和尚数量为0。
        if(state.leftMonk < 0 || state.leftMonster < 0 || state.rightMonk < 0 || state.rightMonster < 0){
            return false;
        }
        //不管是左岸还是右岸,和尚数量大于妖怪数量或者和尚全部在河对岸则有效,船也只能从对岸来回开
        return (state.leftMonk == 0 || state.leftMonk >= state.leftMonster)
                && (state.rightMonk == 0 || state.rightMonk >= state.rightMonster) && state.boatLocation !=state.preState.boatLocation;
    }

    /**
     * 判断是否成功
     */
    public static boolean isSuccess(State state){
        return state.leftMonk == 0 && state.leftMonster == 0;
    }


    /**
     * 广度优先搜索方式解决渡河问题
     */
    public static void crossRiver(State initState){
        //访问的节点队列
        Queue<State> queue = new LinkedList<>();
        queue.add(initState);
        //访问过的状态集合
        Set<State> visited = new HashSet<>();
        visited.add(initState);
        List<State> nextStates;
        while (!queue.isEmpty()){
            State currentState = queue.poll();
            //成功打印路径并退出
            if(isSuccess(currentState)) {
                printPath(currentState);
                return;
            }else {
                nextStates = generateNextStates(currentState);
                for(State nextState : nextStates){
                    //剪枝判断
                    if(!visited.contains(nextState)){
                        //添加到队列中
                        queue.add(nextState);
                        visited.add(nextState);
                    }
                }
            }

        }

    }

    /**
     * 递归打印路径
     */

    public static void  printPath(State state){
        if(state.preState == null){
            return;
        }
        printPath(state.preState);
        System.out.println("从"+(state.preState.boatLocation==-1?"左":"右")+"岸到"+(state.boatLocation==-1?"左":"右")+"岸");
        System.out.println("和尚:"+state.leftMonk+" "+state.rightMonk);
        System.out.println("妖怪:"+state.leftMonster+" "+state.rightMonster);
    }


    public static void main(String[] args) {
        State initState = initState();
        crossRiver(initState);
    }
}

执行结果如下:

从左岸到右岸
和尚:3 0
妖怪:1 2
从右岸到左岸
和尚:3 0
妖怪:2 1
从左岸到右岸
和尚:3 0
妖怪:0 3
从右岸到左岸
和尚:3 0
妖怪:1 2
从左岸到右岸
和尚:1 2
妖怪:1 2
从右岸到左岸
和尚:2 1
妖怪:2 1
从左岸到右岸
和尚:0 3
妖怪:2 1
从右岸到左岸
和尚:0 3
妖怪:3 0
从左岸到右岸
和尚:0 3
妖怪:1 2
从右岸到左岸
和尚:0 3
妖怪:2 1
从左岸到右岸
和尚:0 3
妖怪:0 3

总结

通过广度优先搜索算法,我们成功解决了妖怪和尚过河问题。BFS 的优势在于它能够找到最短路径,适用于状态空间较小的问题。对于更复杂的问题,可以考虑使用其他搜索算法,如深度优先搜索(DFS)算法。我们下篇文章使用DFS求解所有可能的路径。


http://www.kler.cn/a/577527.html

相关文章:

  • java调用c++
  • 深入剖析淘宝商品详情 API 接口 item_get
  • 【机器学习案列】基于随机森林的运动能量消耗预测分析实战
  • 【网络协议详解】——isis技术(学习笔记)
  • 2023年全国职业院校技能大赛网络系统管理赛项模块A:网络构建(样题2)-网络部分解析-附详细代码
  • Qt的QDateTimeEdit控件的使用
  • 【SpringMVC】SpringMVC的启动过程与原理分析:从源码到实战
  • 模型微调——模型性能提升方法及注意事项(自用)
  • TK协议强私——TK采集器
  • 躲藏博弈:概率论与博弈论视角下的最优策略选择
  • 【Find My功能科普】防盗黑科技如何改变生活?
  • 在IDEA中进行git回滚操作:Reset current branch to here‌或Reset HEAD
  • 大白话如何利用 CSS 实现一个三角形?原理是什么?
  • DeepSeek R1-32B医疗大模型的完整微调实战分析(全码版)
  • 不蒜子 UV、PV 统计数据初始化配置
  • Java 开发工具
  • 【VUE2】第三期——样式冲突、组件通信、异步更新
  • 视频理解开山之作 “双流网络”
  • 导入 Excel 规则批量修改或删除 Excel 表格内容
  • 解锁日常养生密码,拥抱健康生活