【人工智能】传教士和野人问题(M-C问题)

https://blog.csdn.net/jiange_zh/article/details/49313787

摘要

本题需要解决的是一般情况下的传教士和野人问题(M-C问题)。通过对问题的一般化,我们用一个三元组定义了问题的状态空间,并根据约束条件制定了一系列的操作规则,最后通过两个启发式函数,来优化搜索过程,并通过讨论,探究两个函数是否能够求解到最优解。

导言

有N个传教士和N个野人来到河边渡河,河岸有一条船,每次至多可供k人乘渡。问传教士为了安全起见,应如何规划摆渡方案,使得任何时刻,河两岸以及船上的野人数目总是不超过传教士的数目(否则不安全,传教士有可能被野人吃掉)。即求解传教士和野人从左岸全部摆渡到右岸的过程中,任何时刻满足M(传教士数)≥C(野人数)和M+C≤k的摆渡方案。

实验过程

状态空间

我们用一个三元组(m,c,b)来表示河岸上的状态,其中m、c分别代表某一岸上传教士与野人的数目,b=1表示船在这一岸,b=0则表示船不在。
约束条件是: 两岸上M≥C, 船上M+C≤2。
由于传教士与野人的总数目是一常数,所以只要表示出河的某一岸上的情况就可以了,为方便起见,我们选择传教士与野人开始所在的岸为所要表示的岸,并称其为左岸,另一岸称为右岸。显然仅用描述左岸的三元组就足以表示出整个情况了。
综上,我们的状态空间可表示为:(ML,CL,BL),其中0≤ML,CL≤N,BL∈{0, 1}。
状态空间的总状态数为(N+1)×(N+1)×2,问题的初始状态是(N,N,1),目标状态是(0,0,0)。

操作规则

该问题主要有两种操作:从左岸划向右岸和从右岸划向左岸,以及每次摆渡的传教士和野人个数。
我们可以使用一个2元组(BM,BC)来表示每次摆渡的传教士和野人个数,我们用i代表每次过河的总人数,i = 1~k,则每次有BM个传教士和BC=i-BM个野人过河,其中BM= 0~i,而且当BM!=0时需要满足BM>=BC。则从左到右的操作为:(ML-BM,CL-BC,B = 1),从右到左的操作为:(ML+BM,CL+BC,B = 0)。
例如当N=3,K=2时,满足条件的(BM,BC)有:
(0,1)、(0,2)、(0,3)、(1,0)、(1,1)、(2,0)、(2,1)、(2,2)、(3,0)、(3,1)、(3,2)、(3,3)。
由于从左到右与从右到左是对称的,所以此时一共有24种操作。

搜索策略

  1. 为了避免重复,我们将搜索过的状态记录下来,之后避开搜索这个状态。
  2. 我们把满足条件的状态称为安全状态,首先要定义出安全状态,通过对问题的分析,不难得出只有满足以下条件之一的状态才是安全的(以左岸为例):
    1)传教士与野人的数目相等;
    2)传教士都在左岸;
    3)传教士都不在左岸。
    我们只对安全的状态进行深度优先搜索,直至找到一个合法的解。
  3. 由于每一次摆渡都有多种操作可以选择,因此我们定义以下启发式函数:
    F1(x) = ML + CL
    F2(x) = ML + CL – 2B
    其中F1(x)满足A算法条件的,F2(x)满足A*算法条件。
    在每次的摆渡中,优先选择F(x)大的操作进行搜索。

结果分析

1.摆渡方案结果示例

样例1:
请输入N:3
请输入k:2
找到的解为:
0个传教士和2个野人从左岸乘船至右岸
左岸有3个传教士和1个野人
右岸有0个传教士和2个野人

0个传教士和1个野人从右岸乘船至左岸
左岸有3个传教士和2个野人
右岸有0个传教士和1个野人

0个传教士和2个野人从左岸乘船至右岸
左岸有3个传教士和0个野人
右岸有0个传教士和3个野人

0个传教士和1个野人从右岸乘船至左岸
左岸有3个传教士和1个野人
右岸有0个传教士和2个野人

2个传教士和0个野人从左岸乘船至右岸
左岸有1个传教士和1个野人
右岸有2个传教士和2个野人

1个传教士和1个野人从右岸乘船至左岸
左岸有2个传教士和2个野人
右岸有1个传教士和1个野人

2个传教士和0个野人从左岸乘船至右岸
左岸有0个传教士和2个野人
右岸有3个传教士和1个野人

0个传教士和1个野人从右岸乘船至左岸
左岸有0个传教士和3个野人
右岸有3个传教士和0个野人

0个传教士和2个野人从左岸乘船至右岸
左岸有0个传教士和1个野人
右岸有3个传教士和2个野人

0个传教士和1个野人从右岸乘船至左岸
左岸有0个传教士和2个野人
右岸有3个传教士和1个野人

0个传教士和2个野人从左岸乘船至右岸
左岸有0个传教士和0个野人
右岸有3个传教士和3个野人

样例2:
请输入N:5
请输入k:3
找到的解为:
0个传教士和2个野人从左岸乘船至右岸
左岸有5个传教士和3个野人
右岸有0个传教士和2个野人

0个传教士和1个野人从右岸乘船至左岸
左岸有5个传教士和4个野人
右岸有0个传教士和1个野人

0个传教士和2个野人从左岸乘船至右岸
左岸有5个传教士和2个野人
右岸有0个传教士和3个野人

0个传教士和1个野人从右岸乘船至左岸
左岸有5个传教士和3个野人
右岸有0个传教士和2个野人

0个传教士和2个野人从左岸乘船至右岸
左岸有5个传教士和1个野人
右岸有0个传教士和4个野人

0个传教士和1个野人从右岸乘船至左岸
左岸有5个传教士和2个野人
右岸有0个传教士和3个野人

3个传教士和0个野人从左岸乘船至右岸
左岸有2个传教士和2个野人
右岸有3个传教士和3个野人

1个传教士和1个野人从右岸乘船至左岸
左岸有3个传教士和3个野人
右岸有2个传教士和2个野人

3个传教士和0个野人从左岸乘船至右岸
左岸有0个传教士和3个野人
右岸有5个传教士和2个野人

0个传教士和2个野人从右岸乘船至左岸
左岸有0个传教士和5个野人
右岸有5个传教士和0个野人

0个传教士和3个野人从左岸乘船至右岸
左岸有0个传教士和2个野人
右岸有5个传教士和3个野人

0个传教士和2个野人从右岸乘船至左岸
左岸有0个传教士和4个野人
右岸有5个传教士和1个野人

0个传教士和3个野人从左岸乘船至右岸
左岸有0个传教士和1个野人
右岸有5个传教士和4个野人

0个传教士和2个野人从右岸乘船至左岸
左岸有0个传教士和3个野人
右岸有5个传教士和2个野人

0个传教士和3个野人从左岸乘船至右岸
左岸有0个传教士和0个野人
右岸有5个传教士和5个野人

 

2.使用启发式函数所花费的实际费用示例:

N,k使用F1(x)花费的实际费用使用F2(x)花费的实际费用
N=3,k=21111
N=5,k=31515
N=25,k=59595
N=50,k=5195195
N=100,k=10409409

下面我们来讨论两个启发式函数求解该问题时能否得到最优解:

首先,F1(x)=M+C不满足A*条件,比如状态(1, 1, 1),F1(x)=M+C=1+1=2,而实际上只要一次摆渡就可以达到目标状态,其最优路径的耗散值为1,所以不满足A*的条件。
  而F2(x)=M+C-2B是满足A*条件的,证明如下:
  先考虑船在左岸的情况。如果不考虑限制条件,也就是说,船一次可以将k个人从左岸运到右岸,然后再有一个人将船送回来。这样,船一个来回可以运过河k-1人,而船仍然在左岸。而最后剩下的k个人,则可以一次将他们全部从左岸运到右岸。所以,在不考虑限制条件的情况下,也至少需要摆渡ceil((2*N-k)/(k-1))*2+1次。其中分子上的”-k”表示剩下k个留待最后一次运过去。除以”k-1”是因为一个来回可以运过去k-1人,需要(2*N-k)/(k-1)个来回,而”来回”数不能是小数,需要向上取整。而乘以”2”是因为一个来回相当于两次摆渡,所以要乘以2。而最后的”+1”,则表示将剩下的k个运过去,需要一次摆渡。
  再考虑船在右岸的情况。同样不考虑限制条件。船在右岸,需要一个人将船运到左岸。因此对于状态(M,C,0)来说,其所需要的最少摆渡数,相当于船在左岸时状态(M+1,C,1)或(M,C+1,1)所需要的最少摆渡数,再加上第一次将船从右岸送到左岸的一次摆渡数。因此所需要的最少摆渡数为:(M+C+1)-2+1 。其中(M+C+1)的”+1”表示送船回到左岸的那个人,而最后边的”+1”,表示送船到左岸时的一次摆渡。
综合船在左岸和船在右岸两种情况下,所需要的最少摆渡次数用一个式子表示为:M+C-2B。其中B=1表示船在左岸,B=0表示船在右岸。由于该摆渡次数是在不考虑限制条件下,推出的最少所需要的摆渡次数。因此,当有限制条件时,最优的摆渡次数只能大于等于该摆渡次数。所以启发函数F2(x)是满足A*条件的。
因此,在有解的情况下,F2(x)在求解本问题时总能找到最优解。对于F1(x),当从左向右摆渡时, F1(x)=F2(x)=M+C,当从右向左摆渡时,F1(x)=M+C,F2(x)=M+C-2,即F1(x)=F2(x)+2,由于我们优先搜索F(x)较大的状态空间,而通过两个函数的关系我们可以知道,他们状态空间的转移是完全一致的。故虽然F1(x)不满足A*条件,但是在本问题中,它也是总能找到最优解。

C++代码

#include <iostream> 
#include <vector>
#include <cmath>

using namespace std;

int X, Y;
int k;

struct node
{
    int q[3];
};

vector<node> s;
int q[500][3];
//用于存放搜索结点,q[][0]是左岸传教士人数
//q[][1]是左岸野蛮人人数,q[][2]是左岸船的数目
//q[][3]用于搜索中的父亲结点序号。
int ans=0;

int op_num = 0;
int go[500][2];
int fx[500][500];

//安全状态:左岸中,传教士都在or都不在or传教士人数等于野人人数 
int is_safe(int state[3])
{
    if ((state[0]==0||state[0]==X||state[0]==state[1])&&(state[1]>=0)&&(state[1]<=Y))
    {
        return 1;
    }
    return 0;
}

//是否到达目标状态 
int is_success(int state[3])
{
    if (state[0]==0&&state[1]==0)
        return 1;
    return 0;
}

//该状态是否已经访问过 
int vis(int state[3])
{
    for (vector<node>::iterator it = s.begin(); it != s.end(); it++)
        if ((*it).q[0] == state[0] && (*it).q[1] == state[1] && (*it).q[2] == state[2])
            return 1;
    return 0;
}

int f1(int state[3])
{
    return state[0]+state[1];
}

int f2(int state[3])
{
    return state[0]+state[1]-2*state[2];    
} 

int find_max(int cur)
{
    int max = -1;
    int op = -1;
    for (int j = 0; j < op_num; j++)//分别考虑可能的动作
    {
        if (fx[cur+1][j] > max)
        {
            max = fx[cur+1][j];
            op = j;
        }           
    }
    if (max == -1)
        op = -1;
    return op;
}

//过河操作
int search(int cur)
{
    if (is_success(q[cur]))
    {
        ans = cur;
        return 1;
    }
    int state[3];
    int j;
    //cout<<"第"<<cur<<"层搜索"<<endl;
    //获取当前搜索结点
    //cout<<"展开结点"<<cur<<":"<<q[cur][0]<<' '<<q[cur][1]<<' '<<q[cur][2]<<endl;
    if (q[cur][2])//船在左边
    {
        for (j = 0; j < op_num; j++)//分别考虑可能的动作
        {
            state[0]=q[cur][0]-go[j][0];
            state[1]=q[cur][1]-go[j][1];
            state[2]=0;//船到了右边
            fx[cur+1][j]=f2(state);
        }
        j = find_max(cur);
        while (j != -1)
        {
            fx[cur+1][j] = -1;
            state[0]=q[cur][0]-go[j][0];
            state[1]=q[cur][1]-go[j][1];
            state[2]=0;//船到了右边
            if (is_safe(state)&&!vis(state))//如果是安全状态//判断与之前展开结点是否相同
            {
                node nd;
                nd.q[0]=q[cur+1][0]=state[0];
                nd.q[1]=q[cur+1][1]=state[1];
                nd.q[2]=q[cur+1][2]=state[2];
                s.push_back(nd);
                //cout<<"合法结点:"<<state[0]<<' '<<state[1]<<' '<<state[2]<<endl;       
                if (search(cur+1))
                    return 1;
            } 
            j = find_max(cur);       
        }
    }
    else    //船在右边
    {
        for (j = 0; j < op_num; j++)//分别考虑可能的动作
        {
            state[0]=q[cur][0]+go[j][0];
            state[1]=q[cur][1]+go[j][1];
            state[2]=1;
            fx[cur+1][j]=f2(state);
        }
        j = find_max(cur);
        while (j != -1)
        {
            fx[cur+1][j] = -1;
            state[0]=q[cur][0]+go[j][0];
            state[1]=q[cur][1]+go[j][1];
            state[2]=1; //船回到左边
            if (is_safe(state)&&!vis(state))//如果是安全状态且与之间状态不同
            {
                node nd;
                nd.q[0]=q[cur+1][0]=state[0];
                nd.q[1]=q[cur+1][1]=state[1];
                nd.q[2]=q[cur+1][2]=state[2];
                s.push_back(nd);
                //cout<<"合法结点:"<<state[0]<<' '<<state[1]<<' '<<state[2]<<endl;
                if(search(cur+1))
                    return 1;
            }
            j = find_max(cur);
        }
    }
    return 0;
}

int main()
{
    int n;
    cout<<"请输入N:";
    cin>>n;
    cout<<"请输入k:";
    cin>>k;
    X = Y = n;

    int state[3];
    //初始状态 
    node nd;
    nd.q[0]=state[0]=q[0][0]=X;
    nd.q[1]=state[1]=q[0][1]=Y;
    nd.q[2]=state[2]=q[0][2]=1;

    s.push_back(nd);
    //初始化操作
    cout<<"合法的操作组有:"<<endl;
    for (int i = 1; i <= k; i++)
        for ( int j = 0; j <= i; j++)
        {
            if (j >= i-j || j == 0)
            {
                go[op_num][0] = j;
                go[op_num][1] = i-j;
                cout<<go[op_num][0]<<' '<<go[op_num][1]<<endl;
                op_num++;               
            }           
        } 
    cout<<endl;
    if (!search(0))
    {
        cout<<"无解"<<endl;
        return 0;
    }
    cout<<"找到的解为:"<<endl;
    for (int i = 0; i <= ans; i++)
    {
        //cout<<q[i][0]<<' '<<q[i][1]<<' '<<q[i][2]<<endl;
        if (i > 0)
        {
            cout<<abs(q[i][0]-q[i-1][0])<<"个传教士和"<<abs(q[i][1]-q[i-1][1])<<"个野人";
            if (q[i][2])
                cout<<"从右岸乘船至左岸"<<endl; 
            else
                cout<<"从左岸乘船至右岸"<<endl; 
            cout<<"左岸有"<<q[i][0]<<"个传教士和"<<q[i][1]<<"个野人"<<endl; 
            cout<<"右岸有"<<n-q[i][0]<<"个传教士和"<<n-q[i][1]<<"个野人"<<endl<<endl; 
        }
    }

    cout<<"本次搜索所花费的费用:"<<ans<<endl;     

    return 0;
}

--------------------- 本文来自 jiange_zh 的CSDN 博客 ,全文地址请点击:https://blog.csdn.net/jiange_zh/article/details/49313787?utm_source=copy

  • 5
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
这个问题可以使用A*算法来解决。首先,我们可以把每个状态看做是一个节点,并建立状态转移图。然后,我们需要定义状态的启发函数(即估计从当前状态到达目标状态所需要的最小代价),以便A*算法可以搜索最优解。 在这个问题中,我们可以使用河岸上的传教士野人数目作为状态的表示。对于每个状态,我们可以计算出从当前状态到达目标状态所需要的最小代价,即当前状态到目标状态所需要的船的往返次数。因此,我们可以定义启发函数为从当前状态到目标状态所需要的最小船的往返次数。这个启发函数可以通过BFS或DFS来计算。 然后,我们可以使用A*算法搜索最优解。在搜索过程中,我们需要维护两个列表:开放列表和关闭列表。开放列表用于保存待扩展的节点,关闭列表用于保存已扩展的节点。首先,将初始状态加入开放列表中。然后,重复以下步骤直到找到目标状态: 1. 从开放列表中选择f值最小的节点进行扩展。 2. 如果当前节点是目标状态,则搜索结束。 3. 否则,将当前节点加入关闭列表中,并根据当前节点生成所有可能的子节点。 4. 对于每个子节点,如果它不在关闭列表中,则计算它的f值并加入开放列表中。 在每个节点中,我们需要记录当前状态、船的位置、已经过河的传教士野人数目等信息。对于每个子节点,我们需要判断它是否符合要求,即传教士的人数多于或等于野人的人数,并且船上的人数不超过K。如果符合要求,则可以将其加入开放列表中。否则,该节点被舍弃。 最后,当搜索结束时,我们可以通过回溯找到从初始状态到达目标状态的路径。这个路径即为问题的解。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值