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

摘要

本题需要解决的是一般情况下的传教士和野人问题(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:
请输入N3
请输入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:
请输入N5
请输入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=2 11 11
N=5,k=3 15 15
N=25,k=5 95 95
N=50,k=5 195 195
N=100,k=10 409 409

下面我们来讨论两个启发式函数求解该问题时能否得到最优解:
首先,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  
#include 
#include 

using namespace std;

int X, Y;
int k;

struct node
{
    int q[3];
};

vector 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::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<<"第"<"层搜索"<//获取当前搜索结点
    //cout<<"展开结点"<":"<<q[cur][0]<<' '<<q[cur][1]<<' '<<q[cur][2]<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]<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]<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<<"合法的操作组有:"<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<0]<<' '<1]<if (!search(0))
    {
        cout<<"无解"<return 0;
    }
    cout<<"找到的解为:"<for (int i = 0; i <= ans; i++)
    {
        //cout<<q[i][0]<<' '<<q[i][1]<<' '<<q[i][2]<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<<"从右岸乘船至左岸"<else
                cout<<"从左岸乘船至右岸"<"左岸有"<<q[i][0]<<"个传教士和"<<q[i][1]<<"个野人"<"右岸有"<q[i][0]<<"个传教士和"<q[i][1]<<"个野人"<"本次搜索所花费的费用:"<return 0;
}

你可能感兴趣的:(人工智能,人工智能,M-C问题,传教士野人问题,启发式搜索)