一、实验目的
1、问题描述
一个农夫带着一只狼、一只羊和一棵白菜,身处河的南岸。他要把这些东西全部运到北岸。他面前只有一条小船,船只能容下他和一件物品,另外只有农夫才能撑船。如果农夫在场,则狼不能吃羊,羊不能吃白菜,否则狼会吃羊,羊会 吃白菜,所以农夫不能留下羊和白菜自己离开,也不能留下狼和羊自己离开,而狼不吃白菜。请求出农夫将所有的东西运过河的方案。
2、实现提示
求解这个问题的简单方法是一步一步进行试探,每一步搜索所有可能的选择 ,对前一步合适的选择后再考虑下一步的各种方案。要模拟农夫过河问题,首先 需要对问题中的每个角色的位置进行描述。可用4位二进制数顺序分别表示农夫、 狼、白菜和羊的位置。用0表在南岸,1表示在北岸。例如,整数5 (0101)表示农 夫和白菜在南岸,而狼和羊在北岸。 现在问题变成:从初始的状态二进制0000(全部在河的南岸)出发,寻找一种 全部由安全状态构成的状态序列,它以二进制1111(全部到达河的北岸)为最终目 标。总状态共16种(0000到1111),(或者看成16个顶点的有向图)可采用广度优先或深度优先的搜索策略---得到从0000到1111的安全路径。
以广度优先为例:整数队列---逐层存放下一步可能的安全状态;Visited[16]数组标记该状态是否已访问过,若访问过,则记录前驱状态值---安全路径。
最终的过河方案应用汉字显示出每一步的两岸状态。
二、算法难点选择用图来做此问题的逻辑结构,判断两个状态之间是否可以相互转换,即模拟过河情景。
三、解决思路及算法
要模拟农夫过河问题,首先需要选择一个对问题中每个角色的位置进行描述的方法。一个很方便的办法是用四位二进制数顺序分别表示农夫、狼、白菜和羊的位置。例如用0表示农夫或者某东西在河的南岸,1表示在河的北岸。因此可以列举出16种情景,其中有6种情形是不安全的。从初始状态二进制0000(全部在河的南岸) 出发,寻找一种全部由安全状态构成的状态序列,它以二进制1111(全部到达河的北岸) 为最终目标,并且在序列中的每一个状态都可以从前一状态通过农夫(可以带一样东西)划船过河的动作到达。
实现DFS的前提是建立图或邻接表进行存储。我们选择用邻接表存储,将问题转化成如何建立邻接表的节点并构建相邻节点。邻接表的每个元素表示一个可以安全到达的中间状态。另外还需要一个数据结构记录已被访问过的各个状态,以及已被发现的能够到达当前这个状态的路径。
首先建立结点,包含农夫、狼、羊、白菜四个属性,最初状态均是0。设visited数组对已访问的顶点进行标记(图的遍历),visited的每个分量初始化值均为-1,每当我们在队列中加入一个新状态时,就把顺序表中以该状态作下标的元素的值改为达到这个状态的路径上前一状态的下标值。visited的一个元素具有非负值表示这个状态已访问过,或是正被考虑。最后我们可以利用visited顺序表元素的值建立起正确的状态路径。isSafe函数是确定状态的安全性的。通过位置分布的代码来判断当前状态是否安全,当农夫与羊不在一起时,狼与羊或羊与白菜在一起是不安全的,则返回false,否则返回true。isConnect函数是判断两点是否有边的函数,条件是农夫的状态改变,且狼、羊、白菜中最多有一个状态改变则两个点相互可达。
搜索过程可利用深度优先搜索算法从初始状态二进制0000(全部在河的南岸)出发,寻找一种全部由安全状态构成的状态序列,它以二进制1111(全部到达河的北岸)为最终目标,并且在序列中的每一个状态都可以从前一个状态得到。为避免重复,要求在序列中不出现重复的状态。用数组retPath保存DFS搜索到的路径,即与某顶点到下一顶点的路径。
四、测试样例
平均时间复杂度为O(n²),空间复杂度为O(n²)。
五、反思启发
最开始拿到这道题,我们都能用自己的语言来描述怎么做才能达到目的,但并不知道怎样用计算机语言来实现这个过程。用这种四元组形式的结点来表示图是十分巧妙的方法。0和1代表每种生物不同的状态,它或者在北岸,或者在南岸,总共有16个结点,如果这样保持16个结点,并在下面的函数中每次都判断这个结点的状态是不是安全的,这使其很麻烦,所以在开始的时候直接筛选出安全状态的结点,不再考虑不安全的,把图的结点数直接降到10个。这道题极大的锻炼了我对图的理解,对建立图,DFS算法都有了进一步的认识。
六、代码附录
#include<iostream>
using namespace std;
const int VertexNum = 16; //最大顶点数typedef struct // 图的顶点
typedef struct
{
int farmer; // 农夫
int wolf; // 狼
int sheep; // 羊
int veget; // 白菜
}Vertex;
typedef struct
{
int vertexNum; // 图的当前顶点数
Vertex vertex[VertexNum]; // 顶点向量(代表顶点)
bool Edge[VertexNum][VertexNum]; // 邻接矩阵. 用于存储图中的边,其矩阵元素个数取决于顶点个数,与边数无关
}AdjGraph; // 定义图的邻接矩阵存储结构
bool visited[VertexNum] = {false}; //
int retPath[VertexNum] = {-1}; // 保存DFS搜索到的路径,即与某顶点到下一顶点的路径
int locate(AdjGraph *graph, int farmer, int wolf, int sheep, int veget)
{
for (int i = 0; i < graph->vertexNum; i++)
{
if ( graph->vertex[i].farmer == farmer && graph->vertex[i].wolf == wolf && graph->vertex[i].sheep == sheep && graph->vertex[i].veget == veget )
return i; //返回当前位置
}
return -1;
}
bool isSafe(int farmer, int wolf, int sheep, int veget)
{
//当农夫与羊不在一起时,狼与羊或羊与白菜在一起是不安全的
if ( farmer != sheep && (wolf == sheep || sheep == veget) ) return false;
else return true;
}
// 判断状态i与状态j之间是否可转换
bool isConnect(AdjGraph *graph, int i, int j)
{
int k = 0;
if (graph->vertex[i].wolf != graph->vertex[j].wolf) k++;
if (graph->vertex[i].sheep != graph->vertex[j].sheep) k++;
if (graph->vertex[i].veget != graph->vertex[j].veget) k++;
// 以上三个条件不同时满足两个且农夫状态改变时,返回真, 也即农夫每次只能带一件东西过桥
if (graph->vertex[i].farmer != graph->vertex[j].farmer && k <= 1) return true;
else return false;
}
// 创建连接图
void CreateG(AdjGraph *graph)
{ int i = 0; int j = 0;
// 生成所有安全的图的顶点
for (int farmer = 0; farmer <= 1; farmer++)
{
for (int wolf = 0; wolf <= 1; wolf++)
{
for (int sheep = 0; sheep <= 1; sheep++)
{
for (int veget = 0; veget <= 1; veget++)
{
if (isSafe(farmer, wolf, sheep, veget))
{
graph->vertex[i].farmer = farmer;
graph->vertex[i].wolf = wolf;
graph->vertex[i].sheep = sheep;
graph->vertex[i].veget = veget;
i++;
}
}
}
}
}
// 邻接矩阵初始化即建立邻接矩阵
graph->vertexNum = i;
for (i = 0; i < graph->vertexNum; i++)
{
for (j = 0; j < graph->vertexNum; j++)
{
// 状态i与状态j之间可转化,初始化为1,否则为0
if (isConnect(graph, i, j)) graph->Edge[i][j] = graph->Edge[j][i] = true;
else graph->Edge[i][j] = graph->Edge[j][i] = false;
}
}
return;
}
// 判断在河的哪一边
char* judgement(int state)
{
if(state == 0) return "左岸";
else return "右岸" ;
}
// 输出从u到v的简单路径,即顶点序列中不重复出现的路径
void printPath(AdjGraph *graph, int start, int end)
{
int i = start;
cout << "farmer" << ", wolf" << ", sheep" << ", veget" << endl;
while (i != end) {
cout << "(" << judgement(graph->vertex[i].farmer) << ", " <<
judgement(graph->vertex[i].wolf)<< ", " << judgement(graph->vertex[i].sheep) << ", " <<
judgement(graph->vertex[i].veget) << ")";
cout << endl;
i = retPath[i]; }
cout << "(" << judgement(graph->vertex[i].farmer) << ", " <<
judgement(graph->vertex[i].wolf)<< ", " << judgement(graph->vertex[i].sheep) << ", " <<
judgement(graph->vertex[i].veget) << ")";
cout << endl;
}
// 深度优先搜索从u到v的简单路径 //DFS--Depth First Search
void dfsPath(AdjGraph *graph, int start, int end)
{
int i = 0;
visited[start] = true; //标记已访问过的顶点
if (start == end) return;
for (i = 0; i < graph->vertexNum; i++)
{
if (graph->Edge[start][i] && !visited[i])
{
retPath[start] = i;
dfsPath(graph, i, end);
}
}
}
int main()
{
AdjGraph graph;
CreateG(&graph);
int start = locate(&graph, 0, 0, 0, 0);
int end = locate(&graph, 1, 1, 1, 1);
dfsPath(&graph, start, end);
if (visited[end]) // 有结果
{printPath(&graph, start, end);
return 0;
}
return -1;
}