您好,欢迎来到化拓教育网。
搜索
您的当前位置:首页传教士问题

传教士问题

来源:化拓教育网


一. 问题描述

有M个传教士和N个野人来到河边准备渡河,河岸有一条船,每次至多可供k人乘渡。任何时刻在河的两岸以及船上的野人数目总是不超过传教士的数目。

二. 问题分析

本问题采用A*算法求解,解答的关键与难点如下:

1. 评估函数的建立。评估函数为f=h+d=M+N-2*B+d.。

M表示左岸的传教士的人数,N表示左岸野人的数目,B取值为0或1 。1表示船在左岸,0 表示船在右岸。d 表示节点的深度。

下面我们来证明h(n)=M+C-2B是满足A*条件的。

我们分两种情况考虑。先考虑船在左岸的情况。如果不考虑条件,也就是说,船一次可以将三人从左岸运到右岸,然后再有一个人将船送回来。这样,船一个来回可以运过河2人,而船仍然在左岸。而最后剩下的三个人,则可以一次将他们全部从左岸运到右岸。所以,在不考虑条件的情况下,也至少需要摆渡[(M+N-3)/2]*2+1次。其中分子上的\"-3\"表示剩下三个留待最后一次运过去。除以\"2\"是因为一个来回可以运过去2人,需要[(M+N-3)/2]个来回,而\"来回\"数不能是小数,需要向上取整,这个用符号[ ]表示。而乘以\"2\"是因为一个来回相当于两次摆渡,所以要乘以2。而最后的\"+1\",则表示将剩下的3个运过去,需要一次摆渡。

化简有: M+N-2。

再考虑船在右岸的情况。同样不考虑条件。船在右岸,需要一个人将船运到左岸。因此对于状态(M,N,0)来说,其所需要的最少摆渡数,相当于船在左岸时状态(M+1,N,1)或(M,N+1,1)所需要的最少摆渡数,再加上第一次将船从右岸送到左岸的一次摆渡数。因此所需要的最少摆渡数为:(M+N+1)-2+1。其中(M+N+1)的\"+1\"表示送船回到左岸的那个人,而最后边的\"+1\",表示送船到左岸时的一次摆渡。

化简有:(M+N+1)-2+1=M+N。

综合船在左岸和船在右岸两种情况下,所需要的最少摆渡次数用一个式子表示为:M+N-2B。其中B=1表示船在左岸,B=0表示船在右岸。

由于该摆渡次数是在不考虑条件下,推出的最少所需要的摆渡次数......

从前有一条河,河的左岸有m个传教士(Missionary)和m个野人(Cannibal),和一艘最多可乘n人的小船。约定左岸,右岸和船上或者没有传教士,或者野人数量少于传教士,否则野人会把传教士吃掉。

编程,接收m和n,搜索一条可让所有的野人和传教士安全渡到右岸的方案。

我们先假设左岸有3个传教士和3个野人,小船最多可乘2人。把当前左岸的状态抽象为:

(3,3,1)

前两个\"3\"代表左岸有3个传教士和3个野人,1代表船在左岸。把每一次可行的渡船方案作为算符。比如,在初始状态,让1个传教士和1个野人上船并渡到右岸,这一算符可表示为:

(1,1)

算符的两位数分别代表要移动的传教士,野人的数量;把人移到没有船的岸边并且改变状态向量中船的值。

对于固定大小的小船,算符的数量是一定的:

class Move {

public:

int missionary; //要移动的传教士数量

int cannibal; //野人

};

class MoveGroup {

public:

Move move[500]; //算符集

int numMove; //可用算符的总数

MoveGroup(int MAX_ON_BOAT) { //利用构造器求算符集

int m, c, i = 0;

for (m = 0; m <= MAX_ON_BOAT; m++)

for (c = 0; c <= MAX_ON_BOAT; c++)

if (c==0 && m!=0) {

move[i].missionary=m;

move[i].cannibal=0;

i++;

}

else if (m==0 && c!=0) {

move[i].missionary=0;

move[i].cannibal=c;

i++;

}

else if (m+c<=MAX_ON_BOAT && m+c!=0 && m>=c) {

move[i].missionary = m;

move[i].cannibal = c;

i++;

}

numMove = i;

}

};

创建一个MoveGroup 对象

MoveGroup mg(2);

即可得到当小船最多可乘2人时的算符集。

这个程序所要做的,就是通过这个已知的算符集,将初始状态(3,3,1)转变为最终状态(0,0,0)。我们应将状态作为搜索的元素。

构建类时应注意,并不是每个算符对于任意的状态都是可以应用的,这需要对应用算符后的安全性进行检查,以判断这一算符对当前状态是否可用;同时,类中也要包含一个判断当前状态是否是最终节点(0,0,0)的方法;当然”==”,”=”这两个运算符以及null值,这是调用dso.h时所不可或缺的。(详见源文件)

class ElemType : Move { //继承Move类,获得传教士,野人数据成员。

private:

bool boat; //船是否在左岸?

public:

ElemType* flag; //这个后边再说,暂时用不到

ElemType(int MAX_PEOPLE) { //创建初始状态

missionary = cannibal = MAX_PEOPLE;

boat = true;

flag = NULL;

}

ElemType() {}

bool operator ==(ElemType e) {

return this->missionary==e.missionary &&

this->cannibal==e.cannibal &&

this->boat==e.boat;

}

void operator =(ElemType e) {

this->missionary = e.missionary;

this->cannibal = e.cannibal;

this->boat = e.boat;

this->flag = e.flag;

}

ElemType friend operator >>(ElemType source, Move &mv) {

//移动操作,通过重载运算符“>>”,你将在isSafe(ElemType)中见到用法。

ElemType result;

if(source.boat == 1) {

result.missionary = (source.missionary -= mv.missionary);

result.cannibal = (source.cannibal -= mv.cannibal);

result.boat = 0;

} else {

result.missionary = (source.missionary += mv.missionary);

result.cannibal = (source.cannibal += mv.cannibal);

result.boat = 1;

}

return result;

}

bool isSafe(Move &mv, int MAX_PEOPLE) {

//判断当前状态在进行mv操作后还是不是安全状态

if( (boat==1&&(missionary-mv.missionary < 0 ||

cannibal-mv.cannibal < 0)) ||

(boat==0&&(missionary+mv.missionary > MAX_PEOPLE ||

cannibal+mv.cannibal > MAX_PEOPLE)) )

return false;

else {

ElemType temp = *this >> mv;

if( temp.missionary==0 || temp.missionary==MAX_PEOPLE ||

(temp.missionary>=temp.cannibal &&

MAX_PEOPLE-temp.missionary >= MAX_PEOPLE-temp.cannibal))

return true;

else return false;

}

}

bool isSuccess() { return missionary==0 && cannibal==0 && boat==0; }

//isSuccess()判断当前状态是否为最终节点

int getM() { return missionary; }

int getC() { return cannibal; }

int getB() { return boat; }

void print() {

cout <<'('<< missionary <<','<< cannibal

<<','<< boat <<')' << endl;

}

};

const ElemType null(0); //(0,0,1) 这是不会出现的

void print(ElemType &e) { e.print(); } //打印函数

至此,我们已经完成了对问题的描述。

搜索过程采用较简单的“宽度优先盲目搜索”,算法框图如下:

#include \"dso.h\"

typedef ElemType Status;

open,closed表均通过队列实现。由于对扩展节点要保存指针,所以closed表需要一个获得尾指针的方法。

class Queuex : public Queue {

public:

ElemType* getTailPtr() {

if(Queue::isEmpty()) return NULL;

QNode* temp = front->next;

while(temp != rear) {

if(temp->next == rear) return &temp->next->node;

temp = temp->next;

}

return &temp->node;

}

};

当得到了一个最终节点(0,0,0)时,如果我们前边的操作没有保存路径的话,那么我们就只知道这个问题有解,而不知道解的具体路径。还记得在定义ElemType时那个旗子指针吗,用它保留它的父节点的地址,问题就解决了。

搜索得到的答案也保存在一个队列里,但我们知道:当得到一个最终节点时,根据它的指向父节点的指针向上搜索得到的是一个反的解序列,这里使用一个临时的栈,可以将解的顺序调换过来。

class Answer : public Queue {

private:

int max_people;

public:

Answer(int MAX_PEOPLE, int MAX_ON_BOAT) {

Queue open;

Queuex closed;

Stack temp; //这是所使用的临时栈

Status s0(MAX_PEOPLE); //这是初始节点

MoveGroup mg(MAX_ON_BOAT); //这是算符集

//以下是搜索算法:

open.enqueue(s0);

max_people = MAX_PEOPLE;

while(!open.isEmpty()) {

Status s1 = open.dequeue(), s2;

closed.enqueue(s1);

ElemType* ptr = closed.getTailPtr();

//得到被扩展的父节点的指针ptr

for(int i = 0; i < mg.numMove; i++)

if( s1.isSafe(mg.move[i], MAX_PEOPLE) ) {

s2 = s1 >> mg.move[i];

if(closed.locate(s2) == INFEASIBLE) {

s2.flag = ptr; open.enqueue(s2);

}

if(s2.isSuccess()) {

//搜索到最终节点后的操作

closed.enqueue(s2);

while(s2.flag != NULL) {

temp.push(s2); s2 = *(s2.flag);

}

this->enqueue(s0);

while(!s2.isSuccess()) {

s2 = temp.pop(); this->enqueue(s2);

}

return;

}

}

}

}

void show() { this->traverse(print); }

};

就要成功了,通过下面这个声明就可以得到MAX_PEOPLE个传教士,MAX_PEOPLE个野人,每艘船上最多乘坐MAX_ON_BOAT人的解:

Answer answer(MAX_PEOPLE, MAX_ON_BOAT);

通过下面的调用可以列出整个解的过程:

answer.show();

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- huatuo9.cn 版权所有 赣ICP备2023008801号-1

违法及侵权请联系:TEL:199 18 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务