广州大学学生实验报告
开课学院及实验室:计算机学院/电子信息楼 416B 2022年 9 月 28日
学 院 | 计算机科学与网络工程学院 | 年级/专业/班 | 计科 | 姓名 | Great Macro | 学号 | |
实验课程名称 | 人工智能原理实验 | 成绩 | |||||
实验项目名称 | 实验1知识的表示与推理实验 | 指导老师 |
实验一 知识的表示与推理实验
一、实验目的
本实验课程是计算机、智能、物联网等专业学生的一门专业课程,通过实验,帮助学生更好地掌握人工智能相关概念、技术、原理、应用等;通过实验提高学生编写实验报告、总结实验结果的能力;使学生对智能程序、智能算法等有比较深入的认识。本实验通过不同知识的表达与推理问题,强化学生对知识表示的了解和应用,为人工智能后续环节的课程奠定基础。
二、基本要求
- 实验前,复习《人工智能》课程中的有关内容。
- 准备好实验数据。
- 编程或验证需要独立完成,程序应加适当的注释。
- 完成实验报告。
三、实验软件
使用C或C++(Visual studio平台等),或其它语言,如matlab或Python。
四、实验内容:
- 猴子摘香蕉问题
利用一阶谓词逻辑求解猴子摘香蕉问题:房内有一个猴子,一个箱子,天花板上挂了一串香蕉,其位置如图1所示,猴子为了拿到香蕉,它必须把箱子搬到香蕉下面,然后再爬到箱子上。请定义必要的谓词,列出问题的初始化状态(即下图所示状态),目标状态(猴子拿到了香蕉,站在箱子上,箱子位于位置b)。(附加:从初始状态到目标状态的谓词演算过程。)
图1
定义描述环境状态的谓词。
AT(x,w):x在w处,个体域:x?{monkey},w?{a,b,c,box};
HOLD(x,t):x手中拿着t,个体域:t?{box,banana};
EMPTY(x):x手中是空的;
ON(t,y):t在y处,个体域:y?{b,c,ceiling};
CLEAR(y):y上是空的;
BOX(u):u是箱子,个体域:u?{box};
BANANA(v):v是香蕉,个体域:v?{banana};
使用谓词、连结词、量词来表示环境状态。
问题的初始状态可表示为:So:AT(monkey,a)?EMPTY(monkey)?ON(box,c)?ON(banana,ceiling)?CLEAR(b)?BOX(box)?
BANANA(banana)
要达到的目标状态为:Sg:
AT(monkey,box)?HOLD(monkey,banana)?ON(box,b)?CLEAR(ceiling)?CLEAR(c)?
BOX(box)?BANANA(banana)
从初始状态到目标状态的转化, 猴子需要完成一系列操作, 定义操作类谓词表示其动作。
WALK(m,n):猴子从m走到n处,个体域:m,n?{a,b,c};
CARRY(s,r):猴子在r处拿到s,个体域:r?{c,ceiling},s?{box,banana};
CLIMB(u,b):猴子在b处爬上u;
这3个操作也可分别用条件和动作来表示。条件直接用谓词公式表示,是为完成相应操作所必须具备的条件;当条件中的事实使其均为真时,则可激活操作规则,于是可执行该规则中的动作部分。动作通过前后状态的变化表示,即通过从动作前删除或增加谓词公式来描述动作后的状态。
WALK(m,n):猴子从m走到n处
条件:AT(monkey,m)
动作:
CARRY(s,r):猴子在r处拿到s
条件:AT(monkey,r)?EMPTY(monkey)?ON(s,r)?BOX(box)?BANANA(banana)
动作:
CLIMB(u,b):猴子在b处爬上u
条件:AT(monkey,b)?HOLD(monkey,u)?CLEAR(b)?BOX(box)?BANANA(banana)
动作:
按照行动计划, 一步步进行状态替换, 直至目标状态
AT(monkey,a)?EMPTY(monkey)?ON(box,c)?ON(banana,ceiling)?CLEAR(b)?BOX(box)?
BANANA(banana)
AT(monkey,c)?EMPTY(monkey)?ON(box,c)?ON(banana,ceiling)?CLEAR(b)?BOX(box)?
BANANA(banana)
AT(monkey,c)?HOLD(monkey,box)?ON(banana,ceiling)?CLEAR(b)?CLEAR(c)?BOX(box)?
BANANA(banana)
AT(monkey,b)?HOLD(monkey,box)?ON(banana,ceiling)?CLEAR(b)?CLEAR(c)?BOX(box)?
BANANA(banana)
AT(monkey,box)?EMPTY(monkey)?ON(box,b)?ON(banana,ceiling)?CLEAR(c)?BOX(box)?
BANANA(banana)
AT(monkey,box)?HOLD(monkey,banana)?ON(box,b)?CLEAR(ceiling)?CLEAR(c)?BOX(box)?
BANANA(banana)(目标得解)
猴子行动的规则序列是:WALK(a,c)→CARRY(c,box)→WALK(c,b)→CLIMB(box,b)→
CARRY(banana,ceiling)
参考代码:
#include <stdio.h> struct State { int monkey; /*-1:Monkey at A;0: Monkey at B;1:Monkey at C;*/ int box; /*-1:box at A;0:box at B;1:box at C;*/ int banana; /*Banana at B,Banana=0*/ int monbox; /*-1: monkey on the box;1: monkey the box;*/ }; struct State States [150]; char* routesave[150]; /*function monkeygoto,it makes the monkey goto the other place*/ void monkeygoto(int b,int i) { int a; a=b; if (a==-1) { routesave[i]="Monkey go to A"; States[i+1]=States[i]; States[i+1].monkey=-1; } else if(a==0) { routesave[i]="Monkey go to B"; States[i+1]=States[i]; States[i+1].monkey=0; } else if(a==1) { routesave[i]="Monkey go to C"; States[i+1]=States[i]; States[i+1].monkey=1; } else { printf("parameter is wrong"); } } /*end function monkeyygoto*/ /*function movebox,the monkey move the box to the other place*/ void movebox(int a,int i) { int B; B=a; if(B==-1) { routesave[i]="monkey move box to A"; States[i+1]=States[i]; States[i+1].monkey=-1; States[i+1].box=-1; } else if(B==0) { routesave[i] = "monkey move box to B"; States[i+1]=States[i]; States[i+1].monkey=0; States[i+1].box=0; } else if(B==1) { routesave[i] = "monkey move box to C"; States[i+1]=States[i]; States[i+1].monkey=1; States[i+1].box=1; } else { printf("parameter is wrong"); } } /*end function movebox*/ /*function climbonto,the monkey climb onto the box*/ void climbonto(int i) { routesave[i]="Monkey climb onto the box"; States[i+1]=States[i]; States[i+1].monbox=1; } /*function climbdown,monkey climb down from the box*/ void climbdown(int i) { routesave[i]="Monkey climb down from the box"; States[i+1]=States[i]; States[i+1].monbox=-1; } /*function reach,if the monkey,box,and banana are at the same place,the monkey reach banana*/ void reach(int i) { routesave[i]="Monkey reach the banana"; } /*output the solution to the problem*/ void showSolution(int i) { int c; printf ("%s \n", "Result to problem:"); for(c=0; c<i+1; c++) { printf ("Step %d : %s \n",c+1,routesave[c]); } printf("\n"); } /*perform next step*/ void nextStep(int i) { int c; int j; if(i>=150) { printf("%s \n", "steplength reached 150,have problem "); return; } for (c=0; c<i; c++) /*if the current state is same to previous,retrospect*/ { if(States[c].monkey==States[i].monkey&&States[c].box==States[i].box&&States[c].banana==States[i].banana&&States[c].monbox==States[i].monbox) { return; } } if(States[i].monbox==1&&States[i].monkey==0&&States[i].banana==0&&States[i].box==0) { showSolution(i); printf("Press any key to continue \n"); getchar();/*to save screen for user,press any key to continue*/ return; } j=i+1; if(States[i].monkey==0) { if(States[i].box==0) { if(States[i].monbox==-1) { climbonto(i); reach(i+1); nextStep(j); /*monkeygoto(-1,i); nextStep(j); monkeygoto(0,i); nextStep(j); movebox(-1,i); nextStep(j); movebox(0,i); nextStep(j);*/ } else { reach(i+1); nextStep(j); /*climbdown(i); nextStep(j);*/ } } else if(States[i].box==1) { /*monkeygoto(-1,i); nextStep(j);*/ monkeygoto(1,i); nextStep(j); movebox(0,i); nextStep(j); climbonto(i); reach(i+1); nextStep(j); } else /*box==-1*/ { monkeygoto(-1,i); nextStep(j); movebox(0,i); nextStep(j); climbonto(i); reach(i+1); nextStep(j); } } /*end if*/ if(States[i].monkey==-1) { if(States[i].box==-1) { if(States[i].monbox==-1) { movebox(0,i); nextStep(j); climbonto(i); reach(i+1); nextStep(j); } else { climbdown(i); nextStep(j); movebox(0,i); nextStep(j); climbonto(i); reach(i+1); nextStep(j); } } else if(States[i].box==0) { monkeygoto(0,i); nextStep(j); climbonto(i); reach(i+1); nextStep(j); } else { monkeygoto(1,i); nextStep(j); movebox(0,i); nextStep(j); climbonto(i); reach(i+1); nextStep(j); } } /*end if*/ if(States[i].monkey==1) { if (States[i].box==1) { if(States[i].monbox==-1) { movebox(0,i); nextStep(j); climbonto(i); reach(i+1); nextStep(j); } else { climbdown(i); nextStep(j); movebox(0,i); nextStep(j); climbonto(i); reach(i+1); nextStep(j); } } else if(States[i].box==-1) { monkeygoto(-1,i); nextStep(j); movebox(0,i); nextStep(j); movebox(0,i); nextStep(j); climbonto(i); reach(i+1); nextStep(j); } else { monkeygoto(0,i); nextStep(j); movebox(0,i); nextStep(j); climbonto(i); reach(i+1); nextStep(j); } } /*end if*/ }/*end nextStep*/ int main() { States[0].monkey=-1; States[0].box=1; States[0].banana=0; States[0].monbox=-1; nextStep(0); }
- 传教士(牧师)与野人问题
问题描述:
有n个牧师和n个野人准备渡河,但只有一条能容纳c个人的小船,为了防止野人侵犯牧师,要求无论在何处,牧师的人数不得少于野人的人数(除非牧师人数为0),且假定野人与牧师都会划船,试设计一个算法,确定他们能否渡过河去,若能,则给出小船来回次数最少的最佳方案。
实验步骤:
输入:牧师人数(即野人人数):n;小船一次最多载人量:c。
输出:若问题无解,则显示Failed,否则,显示Successed输出所有可行方案,并标注哪一组是最佳方案。用三元组(X1, X2, X3)表示渡河过程中的状态。并用箭头连接相邻状态以表示迁移过程:初始状态->中间状态->目标状态。
例:当输入n=2,c=2时,输出:221->200->211->010->021->000;
其中:X1表示起始岸上的牧师人数;X2表示起始岸上的野人人数;X3表示小船现在位置(1表示起始岸,0表示目的岸)。
要求:写出算法的设计思想和源程序,并有用户界面实现人机交互(控制台或者窗口都可以),进行输入和输出结果,如:
Please input n: 2 Please input c: 2
Optimal Procedure: 221->200->211->010->021->000
Successed or Failed?: Successed
五、学生实验报告要求
(1)对于猴子摘香蕉问题根据自己编写代码或者参考代码,用不同的初始状态测试代码,记录每种初始状态下的输出结果,并对每个结果进行解释。
源程序代码:
#include <stdio.h> #define Yes 1 // monkey on the box #define No 0 // monkey doesn't on the box #define maxStep 150 // monkey can do the max steps // define the problem states struct struct States { char box; // box location char monkey; // monkey location char banana; // banana location char Onbox; // monkey on the box }states[maxStep]; static int n = 0; // count step // initialize problem states void init() { printf("please input the banana location: "); scanf("%c", &states[0].banana); printf("please input the monkey location: "); scanf("\n%c", &states[0].monkey); printf("please input the box location: "); scanf("\n%c", &states[0].box); // if the monkey and box aren't in the same place, // then predicts monkey doesn't on the box if(states[0].monkey != states[0].box) { states[0].Onbox = No; } else { printf("Is the monkey on the box now? (1/0)"); scanf("\n%hd", &states[0].Onbox); } } // monkey reach the banana void reach(int i) { printf("monkey reach the banana\n"); n++; } // monkey go to location void monkeyGoto(int i, char location) { printf("monkey go to %c\n", location); states[i+1] = states[i]; states[i+1].monkey = location; // change monkey location n++; } // monkey climb on the box void climbOnbox(int i) { printf("monkey climb on the box\n"); states[i+1] = states[i]; states[i+1].Onbox = Yes; n++; } // monkey climb down the box void climbDownbox(int i) { printf("monkey climb down the box\n"); states[i+1] = states[i]; states[i+1].Onbox = No; n++; } // monkey move the box to dest void moveBox(int i, char dest) { printf("monkey move the box to %c\n", dest); states[i+1] = states[i]; states[i+1].monkey = dest; // change monkey location states[i+1].box = dest; // change box location n++; } // monkey do next Step int nextStep(int i) { if (i >= maxStep) { printf("Steps are too more, monkey can't go again!/n"); return No; } // if the banana and box are in the same place if(states[i].banana == states[i].box) { // if the monkey is on the box, then reach it if(states[i].Onbox) { reach(i); return Yes; } else { // monkey doesn't on the box but in the same place if(states[i].monkey == states[i].box) { climbOnbox(i); return nextStep(i+1); } // monkey doesn't on the box and not in the same place else { monkeyGoto(i, states[i].box); return nextStep(i+1); } } } // banana and box aren't in the same place else { // monkey on the box if(states[i].Onbox) { climbDownbox(i); return nextStep(i+1); } else { // monkey and box are in the same place if(states[i].monkey == states[i].box) { moveBox(i, states[i].banana); return nextStep(i+1); } // monkey and box aren't int the same place else { monkeyGoto(i, states[i].box); return nextStep(i+1); } } } } int nextStep1(int i) { while(i< maxStep) { // if the banana and box are in the same place if(states[i].banana == states[i].box) { // if the monkey is on the box, then reach it if(states[i].Onbox) { reach(i); return Yes; } else { // monkey doesn't on the box but in the same place if(states[i].monkey == states[i].box) { climbOnbox(i); i++; continue; } // monkey doesn't on the box and not in the same place else { monkeyGoto(i, states[i].box); i++; continue; } } } // banana and box aren't in the same place else { // monkey on the box if(states[i].Onbox) { climbDownbox(i); i++; continue; } else { // monkey and box are in the same place if(states[i].monkey == states[i].box) { moveBox(i, states[i].banana); i++; continue; } // monkey and box aren't int the same place else { monkeyGoto(i, states[i].box); i++; continue; } } } } if (i >= maxStep) { printf("Steps are too more, monkey can't go again!/n"); return No; } } int main(void) { init(); printf("\nmonkey go to grasp the banana now\n\n"); nextStep1(0); printf("\nTo reach the banana, monkey need to do %i steps\n", n); return 0; }
运行结果:
结果分析:
香蕉在B处,猴子在A处,箱子在C处,猴子想摘香蕉则需先到C处,然后推动箱子到B处,然后爬上去,最后摘香蕉。
结果分析:
香蕉在A处,猴子在箱子上,箱子在B处,猴子需要先爬下箱子,然后推动箱子到A处,然后再爬上箱子,最后摘香蕉。
(2)完善猴子摘香蕉问题参考代码,参考代码中有什么问题?应该如何修改会更好。
太复杂,而且有bug,无法随便调整猴子,香蕉和箱子的位置。而且伙子摘香蕉的路径不需要保存下来,每当做完一步后就可以打印当前的动作,如果不需要保留状态的话,也不需要用状态数组,只用一个结构体变量就行了。
(3)传教士(牧师)与野人问题,写出状态表示的数据结构,还有每种解的状态迁移图示。
(4)写出传教士(牧师)与野人问题的程序清单(语言不限)
#include <stdio.h> #include <stdlib.h> // 定义状态节点 typedef struct node { int src_x; // 起始岸传教士人数 int src_y; // 起始岸野人人数 int dest_x; // 目的岸传教士人数 int dest_y; // 目的岸野人人数 int location; // 船的状态,-1表示在目的岸,1表示在起始岸 struct node *prev; // 前指针 struct node *next; // 后指针 }node; node *head; // 状态链表的头节点 static int N = 0; // 船的最大载人数 static int X = 0; // 起始岸的传教士人数 static int Y = 0; // 起始岸的野人人数 static int count = 0; // 渡河方案 // 一些函数的声明 node *initList(); void del(node *new); int checkSame(node *p); void display(node *head); int checkNo(int X, int Y); void tail_insert(node *head, node *new); void addnew(int x, int y, int location); void Select(int X, int Y, int location); void goRiver(int x, int y, int location); int checkAll(int X, int Y, int location); int main(void) { head = initList(); // 初始化状态链表 if(!head) { printf("初始化状态链表失败\n"); return -1; } printf("请输入起始岸传教士人数:"); scanf("%d", &X); printf("请输入起始岸野人人数:"); scanf("\n%d", &Y); printf("请输入船的最大载人数:"); scanf("\n%d", &N); // 把起始状态插入到链表中 node *new = initList(); new->src_x = X; new->src_y = Y; new->location = 1; tail_insert(head, new); printf("说明:渡河方案表示格式(起始岸传教士人数,起始岸野人人数,目的岸传教士人数,目的岸野人人数,船的状态)\n"); printf("其中船的状态表示格式(-1表示船在目的岸,1表示船在起始岸)\n"); printf("开始计算渡河方案\n"); printf("计算中,请稍等...\n"); Select(X, Y, 1); printf("渡河方案总共有%d种\n", count); return 0; } // 从起始岸或者目的岸选择人渡河,采用深度优先搜索法和剪枝回溯法 void Select(int X, int Y, int location) { int x, y; // x, y满足以下三条不等式 // x <= X 表示选择出的传教士人数不能超过岸上传教士的人数 // y <= Y 表示选择出的野人人数不能超过岸上野人的人数 // x + y <= N, 即y <= N - x, 表示选择出的总人数不能超过上船的最大载人数 for(x = 0; x <= X; x++) { for(y = 0; y <= Y && y <= N-x; y++) { if((x == 0 && y > 0) || (x > 0 && x >= y)) { // 如果是从起始岸选择,则必须至少选出2个人 // 否则一个人划船没意思,会出现死循环 if(location == 1 && x + y >= 2) { goRiver(x, y, location); } // 如果是从目的岸选择,则必须保证选完人上船后,必须有人留在目的岸上 // 否则从起始岸的划船到达目的岸没意思,会出现四循环 if(location == -1 && head->prev->dest_x + head->prev->dest_y - x - y > 0) { goRiver(x, y, location); } } } } // 本次状态搜索完后,要回溯上一个分支(剪枝回溯法) if(head->next->next != head) { del(head->prev); } } // 判断从岸上选出的人能否渡河 void goRiver(int x, int y, int location) { switch(checkAll(x, y, location)) { // 不能渡河 case 0: return; // 可以渡河 case 1: addnew(x, y, location); // 需要进行转态查重 // 具体例子是(2, 1, 0, 1, 1)->(0, 1, 2, 1, -1)->(2, 1, 0, 1, 1)->(0, 1, 2, 1, -1)... // 不去重会出现死循环,不信可以试试2个传教士,2个野人,船最大载人数为2的情况 if(checkSame(head->prev)) { del(head->prev); // 删除重复的状态 return; } // 全部已经渡河 if(!head->prev->src_x && !head->prev->src_y) { printf("第%d种渡河方案\n", ++count); display(head); // 打印渡河方案 del(head->prev); // 剪枝回溯 return; } // 人还没全部渡河,且下一次从起始岸选择人渡河 if(head->prev->location == 1) { Select(head->prev->src_x, head->prev->src_y, 1); } // 人还没全部渡河,且下一次从目的岸选择人渡河 else { Select(head->prev->dest_x, head->prev->dest_y, -1); } return; // 已经全部渡河 // 不过该条件基本不会出现, 因为case1中有判断下一个状态是否为目的态 case 2: printf("第%d种渡河方案\n", ++count); display(head); // 打印渡河方案 del(head->prev); // 剪枝回溯 return; } } // 检查链表中是否有重复的状态 int checkSame(node *p) { for(node *q = head->next; q != p; q = q->next) { // 只需要该状态的起始岸或者目的岸的人数进行比较,不需要两个岸都要进行比较 if(q->src_x == p->src_x && q->src_y == p->src_y && q->location == p->location) { return 1; } } return 0; } // 检查在起始岸或者目的岸的人数是否合法 int checkNo(int x, int y) { return x > 0 && x < y; } // 检查在起始岸和目的岸的人是否合法 int checkAll(int x, int y, int location) { int src_x, src_y, dest_x, dest_y; src_x = head->prev->src_x; src_y = head->prev->src_y; dest_x = head->prev->dest_x; dest_y = head->prev->dest_y; // 只要起始岸或者目的岸的人数不合法,就不能渡河 if(checkNo(src_x - x * location, src_y - y * location) || checkNo(dest_x + x * location, dest_y + y * location)) { return 0; } // 已经全部渡河,不需要再渡河 else if(location == -1 && src_x == 0 && src_y == 0 && dest_x == X && dest_y == Y) { return 2; } // 本次选择的人可以渡河,但未全部渡河 else { return 1; } } // 把新的状态插入到链表中 void addnew(int x, int y, int location) { node *p = initList(); if(!p) { printf("malloc fail\n"); return; } // 修改状态的信息 // 有个小技巧,关于location的,不需要分起始岸或者目的岸写 p->src_x = head->prev->src_x - x * location; p->src_y = head->prev->src_y - y * location; p->dest_x = head->prev->dest_x + x * location; p->dest_y = head->prev->dest_y + y * location; p->location = -head->prev->location; tail_insert(head, p); } // 生成一个状态节点 node *initList() { node *new = malloc(sizeof(node)); if(!new) { printf("malloc fail!\n"); return NULL; } new->src_x = 0; new->src_y = 0; new->dest_x = 0; new->dest_y = 0; new->location = 0; new->prev = new; new->next = new; return new; } // 打印渡河方案 void display(node *head) { // 链表不存在或者为空 if(!head || head->next == head) { return; } for(node *p = head->next; p != head; p = p->next) { printf("%d, %d, %d, %d, %d\n", p->src_x, p->src_y, p->dest_x, p->dest_y, p->location); } printf("\n"); } // 尾插法,把节点插到链表最末 void tail_insert(node *head, node *new) { new->prev = head->prev; new->next = head; head->prev->next = new; head->prev =new; } // 删除一个节点,本程序通常是链表最末的一个 void del(node *new) { new->prev->next = new->next; new->next->prev = new->prev; new->prev = new; new->next = new; free(new); }
(5)实验结果讨论。
- 当船的最大载人数大于传教士和野人的总人数且传教士人数大于等于野人人数时,可以一次性过河,也可以分开过河。
- 可能会出现无解的情况,例如:传教士和野人起始人数都为2,船的最大载人数为2时,无解。
- 每次从起始岸选择渡河的人数必须大于等于2个,否则无法把人全部渡河,因为必须至少要有1人从目的岸划船回来。
- 每次从目的岸选择渡河的人数只能少于目的岸的总人数,因为必须要有人留在目的岸,才能保证成功渡河,否则本次选择无意义。