【數據結構與算法】狼、羊、菜和農夫過河:使用圖的廣度優先遍歷實現
Java
農夫需要把狼、羊、菜和自己運到河對岸去,只有農夫能夠划船,而且船比較小。除農夫之外每次只能運一種東西。還有一個棘手問題,就是如果沒有農夫看着,羊會偷吃菜,狼會吃羊。請考慮一種方法,讓農夫能夠安全地安排這些東西和他自己過河。
解題思路
學了圖論的廣度優先遍歷算法后,我們可以使用廣度優先遍歷的思想來完成這道題。
首先定義如何表達農夫、狼、羊、菜在河的哪一邊。只有兩種狀態:
- 在河的一邊(假設為東邊)
- 在河的另一邊(假設為西邊)
那么恰好可以用0和1來表達,任務定義如下(使用字符串來表達):
// 人 狼 羊 菜
// 源: 0 0 0 0
//目標: 1 1 1 1
String s = "0000";
String t = "1111";
那接下來程序的任務就是搜索出從s
到t
的過程了。那么如何轉換成圖論問題?
我們知道,0000 代表農夫、狼、羊、菜都在河的東邊,那么下一種狀態可以有如下幾種選擇:
- 東:空狼羊菜 | 西:人空空空(農夫自己過河)
- 東:空空羊菜 | 西:人狼空空(農夫帶狼過河)
- 東:空狼空菜 | 西:人空羊空(農夫帶羊過河)
- 東:空狼羊空 | 西:人空空菜(農夫帶菜過河)
我們根據這個可以繪制一個圖,頂點0000 分別與頂點1000、頂點1100、頂點1010、頂點1001有邊連接;
其中,根據規則在沒有農夫的情況下,狼和羊不能在一起,羊和菜不能在一起,所以排除掉以上的1,2,4選項。那么下一個狀態就是 0101
然后根據這個原理,再往下查找有哪些是可以的:
- 東:人狼空菜 | 西:空空羊空(農夫自己過河)
- 東:人狼羊菜 | 西:空空空空(農夫帶羊過河)
我們根據這個也可以繪制一個圖,頂點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);
}
}
}