【数据结构与算法】狼、羊、菜和农夫过河:使用图的广度优先遍历实现

【数据结构与算法】狼、羊、菜和农夫过河:使用图的广度优先遍历实现

Java
农夫需要把狼、羊、菜和自己运到河对岸去,只有农夫能够划船,而且船比较小。除农夫之外每次只能运一种东西。还有一个棘手问题,就是如果没有农夫看着,羊会偷吃菜,狼会吃羊。请考虑一种方法,让农夫能够安全地安排这些东西和他自己过河。

解题思路

学了图论的广度优先遍历算法后,我们可以使用广度优先遍历的思想来完成这道题。
首先定义如何表达农夫、狼、羊、菜在河的哪一边。只有两种状态:

  1. 在河的一边(假设为东边)
  2. 在河的另一边(假设为西边)
    那么恰好可以用0和1来表达,任务定义如下(使用字符串来表达):
    //      人  狼  羊  菜
    //  源: 0   0   0   0
    //目标: 1   1   1   1
    String s = "0000";
    String t = "1111";

那接下来程序的任务就是搜索出从st的过程了。那么如何转换成图论问题?
我们知道,0000 代表农夫、狼、羊、菜都在河的东边,那么下一种状态可以有如下几种选择:

  1. 东:空狼羊菜 | 西:人空空空(农夫自己过河)
  2. 东:空空羊菜 | 西:人狼空空(农夫带狼过河)
  3. 东:空狼空菜 | 西:人空羊空(农夫带羊过河)
  4. 东:空狼羊空 | 西:人空空菜(农夫带菜过河)

我们根据这个可以绘制一个图,顶点0000 分别与顶点1000、顶点1100、顶点1010、顶点1001有边连接;

其中,根据规则在没有农夫的情况下,狼和羊不能在一起,羊和菜不能在一起,所以排除掉以上的1,2,4选项。那么下一个状态就是 0101
然后根据这个原理,再往下查找有哪些是可以的:

  1. 东:人狼空菜 | 西:空空羊空(农夫自己过河)
  2. 东:人狼羊菜 | 西:空空空空(农夫带羊过河)

我们根据这个也可以绘制一个图,顶点0101 分别与顶点0000、顶点0010有边连接;

然后再根据规则进行查找。

那么我们可以写出下一个状态的算法:

private HashMap<String, String> getNextSta(String sta) {
    HashMap<String, String> nextSta = new HashMap<>();
    char[] chars = sta.toCharArray();
    char backup;
    String n;
    if (chars[0] == '1') {//在1这一侧(东)
        chars[0] = '0'; // 农夫从1到0这一侧
        n = new String(chars);
        if (!isFailed(n)) {
            nextSta.put(n, "农夫从东侧到西侧");
        }
        //------------------------
        backup = chars[1]; // 备份
        if (chars[1] == '1') { // 如果狼在这边
            chars[1] = '0'; // 带狼过去
            n = new String(chars);
            if (!isFailed(n)) {
                nextSta.put(n, "农夫从东侧带狼到西侧");
            }
        }
        chars[1] = backup; // 恢复
        //------------------------
        backup = chars[2]; // 备份
        if (chars[2] == '1') { // 如果羊在这边
            chars[2] = '0'; // 带羊过去
            n = new String(chars);
            if (!isFailed(n)) {
                nextSta.put(n, "农夫从东侧带羊到西侧");
            }
        }
        chars[2] = backup;// 恢复
        //------------------------
        backup = chars[3];// 备份
        if (chars[3] == '1') { // 如果菜在这边
            chars[3] = '0'; // 带菜过去
            n = new String(chars);
            if (!isFailed(n)) {
                nextSta.put(n, "农夫从东侧带菜到西侧");
            }
        }
        chars[3] = backup;// 恢复
    } else if (chars[0] == '0') {
        chars[0] = '1'; // 农夫从0到1这一侧
        n = new String(chars);
        if (!isFailed(n)) {
            nextSta.put(n, "农夫从西侧到东侧");
        }
        //------------------------
        backup = chars[1]; // 备份
        if (chars[1] == '0') { // 如果狼在这边
            chars[1] = '1'; // 带狼过去
            n = new String(chars);
            if (!isFailed(n)) {
                nextSta.put(n, "农夫从西侧带狼到东侧");
            }
        }
        chars[1] = backup; // 恢复
        //------------------------
        backup = chars[2]; // 备份
        if (chars[2] == '0') { // 如果羊在这边
            chars[2] = '1'; // 带羊过去
            n = new String(chars);
            if (!isFailed(n)) {
                nextSta.put(n, "农夫从西侧带羊到东侧");
            }
        }
        chars[2] = backup;// 恢复
        //------------------------
        backup = chars[3];// 备份
        if (chars[3] == '0') { // 如果菜在这边
            chars[3] = '1'; // 带菜过去
            n = new String(chars);
            if (!isFailed(n)) {
                nextSta.put(n, "农夫从西侧带菜到东侧");
            }
        }
        chars[3] = backup;// 恢复
    }
    return nextSta;
}

写出失败的情况(即不可出现的情况)判断算法:

    private boolean isFailed(String sta) {
        char[] part = sta.toCharArray();
        if (part[0] == '0') {
            if (part[1] == '1' && part[2] == '1') { // 狼和羊,没有农夫
                return true;
            }
            if (part[2] == '1' && part[3] == '1') { // 菜和羊,没有农夫
                return true;
            }
        } else if (part[0] == '1') {
            if (part[1] == '0' && part[2] == '0') { // 狼和羊,没有农夫
                return true;
            }
            if (part[2] == '0' && part[3] == '0') { // 菜和羊,没有农夫
                return true;
            }
        }
    }

接下来就是使用图论的广度优先遍历来搜索出最短路径了
使用两个映射来存储遍历路径和描述

HashMap<String, String> pre; // 记录
HashMap<String, String> des; // 描述

针对状态进行广度优先遍历

ArrayList<String> queue = new ArrayList<>();

if (isFailed(s)) {
    return;
}

String sta = s;
queue.add(sta);
pre.put(sta, sta);
while (!queue.isEmpty()) {
    sta = queue.remove(0);

    HashMap<String, String> nextSta = getNextSta(sta);

    for (String curSta : nextSta.keySet()) {
        if (!pre.containsKey(curSta)) { // 如果还没访问过
            queue.add(curSta);
            pre.put(curSta, sta); // 记录父顶点
            des.put(sta + curSta, nextSta.get(curSta));  // 记录过程描述
            if (curSta.equals(t)) {
                return;
            }
        }
    }
}

执行完这个后,就能在pre里找到完成任务的路径了。

最后这里就是整理和输出了


    public Iterable<String> process() {
        ArrayList<String> res = new ArrayList<>();
        if (!pre.containsKey(t)) {
            return res;
        }
        String cur = t;
        while (!cur.equals(s)) {
            res.add(cur);
            cur = pre.get(cur);
        }
        res.add(s);
        Collections.reverse(res);

        ArrayList<String> ret = new ArrayList<>();
        String p = res.get(0);
        for (int i = 1; i < res.size(); i++) {
            ret.add("状态 : " + getStaDes(p));
            ret.add("步骤 : " + i);
            String s = res.get(i);
            ret.add("操作 : " + des.get(p + s));
            p = s;
        }
        ret.add("状态 : " + getStaDes(p));
        return ret;
    }

    private String getStaDes(String sta) {
        char[] s = sta.toCharArray();
        char[] dc = new char[6], xc = new char[6];
        ArrayList<Character> d = new ArrayList<>(), x = new ArrayList<>(), c = null;
        for (int i = 0; i < s.length; i++) {
            if (s[i] == '1') {
                c = d;
            } else {
                c = x;
            }

            if (i == 0) {
                c.add('人');
            } else if (i == 1) {
                c.add('狼');
            } else if (i == 2) {
                c.add('羊');
            } else if (i == 3) {
                c.add('菜');
            }
        }

        dc[0] = '东';
        xc[0] = '西';
        dc[1] = ':';
        xc[1] = ':';

        for (int i = 2; i < dc.length; i++) {
            dc[i] = '空';
        }
        for (int i = 2; i < xc.length; i++) {
            xc[i] = '空';
        }

        for (int i = 0; i < d.size(); i++) {
            dc[i + 2] = d.get(i);
        }
        for (int i = 0; i < x.size(); i++) {
            xc[i + 2] = x.get(i);
        }

        return new String(xc) + " | " + new String(dc);
    }

然后主函数运行调用一下:

    public static void main(String[] args) {
        FarmerTransport ft = new FarmerTransport();
        for (String s : ft.process()) {
            System.out.println(s);
        }
    }

运行结果如下:

"C:\Program Files\JetBrains\IntelliJ IDEA 2019.3.5\jbr\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.3.5\lib\idea_rt.jar=14007:C:\Program Files\JetBrains\IntelliJ IDEA 2019.3.5\bin" -Dfile.encoding=UTF-8 -classpath D:\Project\DataStructureJavaLearn2021\out\production\data_structure_java top.minuy.subject.costom.bfs.FarmerTransport
状态 : 西:人狼羊菜 | 东:空空空空
步骤 : 1
操作 : 农夫从西侧带羊到东侧
状态 : 西:狼菜空空 | 东:人羊空空
步骤 : 2
操作 : 农夫从东侧到西侧
状态 : 西:人狼菜空 | 东:羊空空空
步骤 : 3
操作 : 农夫从西侧带狼到东侧
状态 : 西:菜空空空 | 东:人狼羊空
步骤 : 4
操作 : 农夫从东侧带羊到西侧
状态 : 西:人羊菜空 | 东:狼空空空
步骤 : 5
操作 : 农夫从西侧带菜到东侧
状态 : 西:羊空空空 | 东:人狼菜空
步骤 : 6
操作 : 农夫从东侧到西侧
状态 : 西:人羊空空 | 东:狼菜空空
步骤 : 7
操作 : 农夫从西侧带羊到东侧
状态 : 西:空空空空 | 东:人狼羊菜

Process finished with exit code 0

成功使用图论的广度优先遍历的思想解出本题~

代码


import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;

/**
 * 一道智力题
 * 农夫需要把狼、羊、菜和自己运到河对岸去,
 * 只有农夫能够划船,而且船比较小。
 * 除农夫之外每次只能运一种东西,
 * 还有一个棘手问题,就是如果没有农夫看着:
 * 羊会偷吃菜,狼会吃羊。
 * 请考虑一种方法,
 * 让农夫能够安全地安排这些东西和他自己过河。
 *
 * @author Minuy
 * @time 8:57
 * @date 2021/11/21
 */
public class FarmerTransport {
    //       人  狼  羊  菜
    //  源: 0   0   0   0
    //目标: 1   1   1   1

    String s = "0000";
    String t = "1111";
    HashMap<String, String> pre; // 记录
    HashMap<String, String> des; // 描述

    public FarmerTransport() {
        pre = new HashMap<>();
        des = new HashMap<>();

        ArrayList<String> queue = new ArrayList<>();

        if (isFailed(s)) {
            return;
        }

        String sta = s;
        queue.add(sta);
        pre.put(sta, sta);
        while (!queue.isEmpty()) {
            sta = queue.remove(0);

            HashMap<String, String> nextSta = getNextSta(sta);

            for (String curSta : nextSta.keySet()) {
                if (!pre.containsKey(curSta)) { // 如果还没访问过
                    queue.add(curSta);
                    pre.put(curSta, sta); // 记录父顶点
                    des.put(sta + curSta, nextSta.get(curSta));  // 记录过程描述
                    if (curSta.equals(t)) {
                        return;
                    }
                }
            }
        }
    }

    private HashMap<String, String> getNextSta(String sta) {
        HashMap<String, String> nextSta = new HashMap<>();
        char[] chars = sta.toCharArray();
        char backup;
        String n;
        if (chars[0] == '1') {//在1这一侧(东)
            chars[0] = '0'; // 农夫从1到0这一侧
            n = new String(chars);
            if (!isFailed(n)) {
                nextSta.put(n, "农夫从东侧到西侧");
            }
            //------------------------
            backup = chars[1]; // 备份
            if (chars[1] == '1') { // 如果狼在这边
                chars[1] = '0'; // 带狼过去
                n = new String(chars);
                if (!isFailed(n)) {
                    nextSta.put(n, "农夫从东侧带狼到西侧");
                }
            }
            chars[1] = backup; // 恢复
            //------------------------
            backup = chars[2]; // 备份
            if (chars[2] == '1') { // 如果羊在这边
                chars[2] = '0'; // 带羊过去
                n = new String(chars);
                if (!isFailed(n)) {
                    nextSta.put(n, "农夫从东侧带羊到西侧");
                }
            }
            chars[2] = backup;// 恢复
            //------------------------
            backup = chars[3];// 备份
            if (chars[3] == '1') { // 如果菜在这边
                chars[3] = '0'; // 带菜过去
                n = new String(chars);
                if (!isFailed(n)) {
                    nextSta.put(n, "农夫从东侧带菜到西侧");
                }
            }
            chars[3] = backup;// 恢复
        } else if (chars[0] == '0') {
            chars[0] = '1'; // 农夫从0到1这一侧
            n = new String(chars);
            if (!isFailed(n)) {
                nextSta.put(n, "农夫从西侧到东侧");
            }
            //------------------------
            backup = chars[1]; // 备份
            if (chars[1] == '0') { // 如果狼在这边
                chars[1] = '1'; // 带狼过去
                n = new String(chars);
                if (!isFailed(n)) {
                    nextSta.put(n, "农夫从西侧带狼到东侧");
                }
            }
            chars[1] = backup; // 恢复
            //------------------------
            backup = chars[2]; // 备份
            if (chars[2] == '0') { // 如果羊在这边
                chars[2] = '1'; // 带羊过去
                n = new String(chars);
                if (!isFailed(n)) {
                    nextSta.put(n, "农夫从西侧带羊到东侧");
                }
            }
            chars[2] = backup;// 恢复
            //------------------------
            backup = chars[3];// 备份
            if (chars[3] == '0') { // 如果菜在这边
                chars[3] = '1'; // 带菜过去
                n = new String(chars);
                if (!isFailed(n)) {
                    nextSta.put(n, "农夫从西侧带菜到东侧");
                }
            }
            chars[3] = backup;// 恢复
        }
        return nextSta;
    }

    public Iterable<String> process() {
        ArrayList<String> res = new ArrayList<>();
        if (!pre.containsKey(t)) {
            return res;
        }
        String cur = t;
        while (!cur.equals(s)) {
            res.add(cur);
            cur = pre.get(cur);
        }
        res.add(s);
        Collections.reverse(res);

        ArrayList<String> ret = new ArrayList<>();
        String p = res.get(0);
        for (int i = 1; i < res.size(); i++) {
            ret.add("状态 : " + getStaDes(p));
            ret.add("步骤 : " + i);
            String s = res.get(i);
            ret.add("操作 : " + des.get(p + s));
            p = s;
        }
        ret.add("状态 : " + getStaDes(p));
        return ret;
    }

    private String getStaDes(String sta) {
        char[] s = sta.toCharArray();
        char[] dc = new char[6], xc = new char[6];
        ArrayList<Character> d = new ArrayList<>(), x = new ArrayList<>(), c = null;
        for (int i = 0; i < s.length; i++) {
            if (s[i] == '1') {
                c = d;
            } else {
                c = x;
            }

            if (i == 0) {
                c.add('人');
            } else if (i == 1) {
                c.add('狼');
            } else if (i == 2) {
                c.add('羊');
            } else if (i == 3) {
                c.add('菜');
            }
        }

        dc[0] = '东';
        xc[0] = '西';
        dc[1] = ':';
        xc[1] = ':';

        for (int i = 2; i < dc.length; i++) {
            dc[i] = '空';
        }
        for (int i = 2; i < xc.length; i++) {
            xc[i] = '空';
        }

        for (int i = 0; i < d.size(); i++) {
            dc[i + 2] = d.get(i);
        }
        for (int i = 0; i < x.size(); i++) {
            xc[i + 2] = x.get(i);
        }

        return new String(xc) + " | " + new String(dc);
    }

    private boolean isFailed(String sta) {
        char[] part = sta.toCharArray();
        if (part[0] == '0') {
            if (part[1] == '1' && part[2] == '1') { // 狼和羊,没有农夫
                return true;
            }
            if (part[2] == '1' && part[3] == '1') { // 菜和羊,没有农夫
                return true;
            }
        } else if (part[0] == '1') {
            if (part[1] == '0' && part[2] == '0') { // 狼和羊,没有农夫
                return true;
            }
            if (part[2] == '0' && part[3] == '0') { // 菜和羊,没有农夫
                return true;
            }
        }
        return false;
    }

    public static void main(String[] args) {
        FarmerTransport ft = new FarmerTransport();
        for (String s : ft.process()) {
            System.out.println(s);
        }
    }
}

posted @ 2021-11-21 12:05  清风来叙  阅读(1063)  评论(0编辑  收藏  举报