【数据结构与算法】狼、羊、菜和农夫过河:使用图的广度优先遍历实现
【数据结构与算法】狼、羊、菜和农夫过河:使用图的广度优先遍历实现
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);
}
}
}