【數據結構與算法】狼、羊、菜和農夫過河:使用圖的廣度優先遍歷實現


【數據結構與算法】狼、羊、菜和農夫過河:使用圖的廣度優先遍歷實現

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);
        }
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM