修道士和野人问题:所有解、启发求解、简单界面

一.作业任务
修道士和野人问题:设有三个修道士和3个野人来到河边,打算用一条船从河的左岸渡到河的右岸去。但该船每次只能装载两个人,在任何岸边野人的数目都不得超过修道士的人数,否则修道士就会被野人吃掉。假设野人服从任何一种过河安排,请规划出使全部6人安全过河的方案。
问题提示:应用状态空间表示和搜索方法时,可用(Nm,Nc)来表示状态描述,其中Nm,Nc分别为传教士和野人的人数。初始状态为(3,3),而可能的中间状态为(0,1),(0,2),(0,3),(1,1),(2,1),(2,2),(3,0),(3,1),(3,2)等。
编程实现修道士和野人问题算法,演示算法运行过程(即过河的各步方案)和结果。

二.运行环境
Windows10,codeblocks,myeclipse。
三.算法介绍
修道士与野蛮人问题是深度优先回溯算法问题。回溯算法也叫试探法,它是一种系统地搜索问题的解的方法。用回溯算法解决问题的一般步骤:
1、 针对所给问题,定义问题的解空间,它至少包含问题的一个(最优)解。
2 、确定易于搜索的解空间结构,使得能用回溯法方便地搜索整个解空间 。
3 、以深度优先的方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索。
深度优先遍历的情况和八皇后类似,不过在本问题中要考虑船在两岸的不同情况。本次设计除了满足3名修道士三名野蛮人船只容量为2的情况外,由于该情况可以不考虑船上野蛮人更多的情况(甚至就可以直接初始化船只情况),故拓展了任意数量修道士野蛮人船只容量的问题,并特意编写了两个启发式函数来缩短搜索代价,其中第二个启发式函数为a*算法,并将不同方式的搜索代价求出以比对。
后来也做了java程序界面,由于代码较长单独附在文件夹内不在报告内。
四.程序分析
在深度优先回溯求解时,没有设置变量求解代价和完成步数变量,后来直接再该代码上加入启发式函数,出现卡死(递归太大),后来重现写了一个专门讨论启发函数代价的代码,在改代码中,修改了很多地方包括输出格式、中间过程保存方式、及结构的使用。对于启发式函数的讨论如下:
启发式函数部分加入三个函数:

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;
}

其中f1为普通启发式函数,f2为a算法,find_max函数为使用俩启发式函数以减少工作量的函数,在设计启发式函数的过程中参考了csdn上某博主的代码,但是该博主最后代码设计出现逻辑写反了的错误,已经和他反应该问题:
在这里插入图片描述
关于启发式函数:由于每一次摆渡都有多种操作可以选择,因此我们定义以下启发式函数:
F1(x) = ML + CL
F2(x) = ML + CL – 2B
其中F1(x)满足A算法条件的,F2(x)满足A
算法条件。
在每次的摆渡中,优先选择F(x)大的操作进行搜索。
首先,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人,需要(2N-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
条件,但是在本问题中,它也是总能找到最优解。
简言之,每次采取措施,都以运送最多人数到目标方案为前提!
五.界面截图
1、回溯求解所有情况(修道士3人野蛮人3人船只容量2):
在这里插入图片描述

2、拓展(任意数目,修道士4人野蛮人4人船只容量4为例)
在这里插入图片描述

3、启发式算法,在我的思路中,fx数组用来储存启发式搜索的判断,其中fx[cur+1][j]=2n-f2(state)为最低代价求解,改为fx[cur+1][j]=-f2(state)可获得最大代价求解的情况,下面做比较:
最低代价输入:
在这里插入图片描述
其中下面每行四个数字为我储存的每次搜索过程,其行数即搜索代价,两次结果:
启发式:
在这里插入图片描述
改写a
算法求出最大代价:
在这里插入图片描述
可以看到搜索性能差异非常明显。
4、根据a*改出来的java程序截图:

在这里插入图片描述

六.结果分析
本次作业通过查询资料了解了修道士和野蛮人问题的由来和含义,然后采用回溯算法来完成了这个项目的主体部分,简要输出结果的同时,拓展了更多数量问题和启发式算法。
七.作业体会
纸上得来终觉浅,亲手实践方能将知识真正理解。透彻修道士与野蛮人算法虽然不算是难度偏高的例子,但是对于我这样刚刚了解人工智能的大学生而言,拥有恰到好处的挑战性,通过努力还是可以完成算法并实现它的展示。这个过程让我复习了回溯思想并加深了我对人工智能的理解,此外,对于启发式函数的探索更是让我受益匪浅。
八.参考资料
《人工智能(21世纪高等学校计算机专业实用规划教材)》清华大学出版社
《修道士与野蛮人问题》百度百科
九.附件(源代码)
1、遍历所有方式:

#include <iostream>
#include <vector>
#include <stdio.h>
int x,y,z,w=0,idea=0;//x为修道士初始数目,y为野蛮人初始数目,z为船容量,w为船只载客方式数量,idea为解数目
using namespace std;
int way[100][2];
vector <string> hist;
void print() //打印输出
{
    for(int i=0;i<hist.size();i++)
        cout<<hist[i]<<endl<<endl;
}
int corr(int frair_num,int bar_num,int boat)
{
    if(frair_num<0||frair_num>x) return 0; //修道士数据非法
    if(bar_num<0||bar_num>x) return 0;     //野蛮人数据非法
    if((frair_num<bar_num&&frair_num)||(x-frair_num&&(x-frair_num)<(y-bar_num))) return 0;   //修道士被野蛮人吃掉的情况
    if(boat!=0&&boat!=1) return 0;
    char text[50];
    sprintf(text, "frair_num=%d, bar_num=%d, boat(初始河岸为1,目标河岸为0)=%d", frair_num,bar_num,boat);
    for(int i=0; i<hist.size(); i++)          //此情况之前出现过
        if(text == hist[i])
                return 0;
    hist.push_back(text);
    return  1;

}

void search(int frair_num,int bar_num,int boat)
{
    if((frair_num==0)&&(bar_num==0)&&(boat==0))
    {
        idea++;
        cout<<"第"<<idea<<"种解法如下"<<endl;
        print();
        return;
    }

    for(int i=0;i<w;i++)
    {
        if(boat==1)
        {
            int f=frair_num-way[i][0];
            int b=bar_num-way[i][1];
            int t=boat-1;
            if(corr(f,b,t))
            {
                search(f,b,t);
                hist.pop_back();
            }


        }
         if(boat==0)
        {
            int f=frair_num+way[i][0];
            int b=bar_num+way[i][1];
            int t=boat+1;
            if(corr(f,b,t))
            {
                search(f,b,t);
                hist.pop_back();
            }

        }
    }
}
int main()
{
    char text[50];
    cout<<"请输入修道士数量,野蛮人数量及船只容量"<<endl;
    cin>>x>>y>>z;
    sprintf(text, "frair_num=%d, bar_num=%d, boat(初始河岸为1,目标河岸为0)=%d", x,y,1);
    hist.push_back(text);
    for(int i=z;i>0;i--)  //船上修道士数量非0
    {
        for(int j=0;j<=i,j<=z-i;j++)
        {
            way[w][0]=i;
            way[w][1]=j;
            w++;
        }
    }
    for(int j=1;j<=z;j++)  //船上修道士数量为0
        {
            way[w][0]=0;
            way[w][1]=j;
            w++;
        }
    cout<<"船只上的可行载客方式:"<<endl;
    for(int j=0;j<w;j++)
    {
        cout<<"船上修道士"<<way[j][0]<<"人,"<<"野蛮人"<<way[j][1]<<"人。"<<endl<<endl;
    }
    search(x,y,1);
    cout<<"总方案数量为"<<idea<<endl;
    return 0;

}

2、启发式:

#include <iostream>
#include <vector>
#include <cmath>
using namespace std;

int X, Y;
int k;
int n;int dai=0;
struct node
{
    int q[3];
};

vector<node> s;
int q[500][4];
//用于存放搜索结点,q[][0]是左岸传教士人数
//q[][1]是左岸野蛮人人数,q[][2]是左岸船的数目

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;//船到了右边
            cout<<abs(q[cur][0])<<" "<<abs(q[cur][1])<<" "<<1<<" "<<j<<endl; dai++;
            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; //船回到左边
            cout<<abs(q[cur][0])<<" "<<abs(q[cur][1])<<" "<<0<<" "<<j<<endl; dai++;
            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()
{

    cout<<"请输入修道士野蛮人数:";
    cin>>n;
    cout<<"请输入船只容量:";
    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;
    cout<<"本次搜索所花费的费用:"<<dai<<endl;

    return 0;
}

3、见文件夹

  • 11
    点赞
  • 63
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值