人工智能导论实验指导书.docx
《人工智能导论实验指导书.docx》由会员分享,可在线阅读,更多相关《人工智能导论实验指导书.docx(21页珍藏版)》请在冰豆网上搜索。
人工智能导论实验指导书
实验一基本的搜索技术
【实验目的】
通过运行演示程序,理解深度优先、广度优先、A*算法的原理和运行过程。
【实验内容】
1.分别以深度优先、广度优先、A*算法为例演示搜索过程
2.观察运行过程记录搜索顺序
3.设置不同属性,观察和记录搜索过程的变化
4.分析不同算法的特点
【实验原理】
在知识不完全时,一般不存在成熟的求解算法可以利用,只有利用已有的知识摸索前进,从许多可能的解中寻找真正的解这就是搜索。
即使对于结构性能较好,理论上有算法可依的问题,由于问题本身的复杂性以及计算机在时间、空间上的局限性,往往也需要通过搜索来进行求解。
总的来说搜索策略分为两大类:
盲目搜索和启发式搜索
一、无信息的搜索策略——盲目搜索
在不具有对特定问题的任何有关信息的条件下,按固定的步骤(依次或随即调用操作算子)进行的搜索,它能快速地运用一个操作算子。
盲目搜索中,由于没有可参考的信息,因此只要能匹配的操作算子都须运用,这会搜索更多的状态。
最重要的宽度优先和深度优先是最重要的盲目搜索方法。
1.宽度优先搜索:
从根结点出发,按从低到高的层次顺序搜索,同一层的结点按固定的顺序(例如从左到右、从右到左)搜索。
宽度优先总是先搜索到距离最近的目标结点。
宽度优先搜索不适合用于分支较多的情况。
2.深度优先搜索:
用回溯的思想搜索图。
深度优先搜索适用于分支较多而层次较浅的情况。
二、利用知识引导搜索——启发式搜索
盲目搜索复杂度很大,为了提高算法效率,应该具体问题具体分析,利用与问题有关的信息,从中得到启发而来引导搜索,以达到减少搜索量的目的,这就是启发式搜索。
启发信息:
(1)陈述性启发信息:
一般被用于更准确、更精炼地描述状态,使问题的状态空间缩小,如待求问题的特定状况等属于此类信息
(2)过程性启发信息:
一般被用于构造操作算子,使操作算子少而精如一些规律性知识等属于此类信息
(3)控制性启发信息:
如何选择操作算子
控制性启发信息往往被反映在估价函数之中。
估价函数的任务就是估计待搜索结点的“有希望”程度(或者说估计操作算子的“性能”),并依此给它们排定次序。
估价一个结点的价值,必须综合考虑两方面的因素:
已经付出的代价(实际)和将要付出的代价(估计)
f(n)=g(n)+h(n)
采用这种估价函数的搜索叫A搜索。
令h*(n)为n到目的结点的实际最小代价,如果对任意节点n均有h(n)≤h*(n),则此时的A算法为A*算法。
如某一问题有解,那么利用A*搜索算法对该问题进行搜索则一定能搜索到解,并且一定能搜索到最优的解而结束。
这三种搜索算法都使用了两张表来记录状态信息:
在open表中保留所有已生成而子状态未扩展的状态;在closed表中记录已扩展过子状态的状态。
广度优先搜索算法中的open表采用的是先进先出的“队列”。
深度优先搜索算法中的open表采用的是后进先出的“栈”。
启发式搜索算法中的open表中的元素是按启发估价函数值的大小排列的一个表,每次从表中优先取出启发估价函数值最小的状态加以扩展。
同时并不丢弃其它的状态,而把它们保留在open表中。
【实验步骤】
1.浏览器打开下面网址,单击“StartApplet”按钮进入演示系统
2.单击主菜单中“File”→“LoadSampleGraph”,载入一个例子(例如“简单搜索树”),如图2红色椭圆标注,激活左侧“create”卡,鼠标指针指向右侧图中的结点或边,右击弹出菜单,单击“Properties”(即属性),可以查看或修改结点或边的属性。
结点的heuristics属性表示结点的启发值,边的Edgecost属性表示边的代价。
(A*算法中结点的估价函数值为从开始结点到该结点所有边的代价和再加上该结点的启发值。
如图3所示B的估价值为13。
)
3.查看并记录所有结点的启发值和边的代价。
4.激活左侧的“Solved”卡,如图4红色椭圆标注
5.单击主菜单中“SearchAlgorithms”,分别选定“DepthFirst”、“BreadthFirst”、“A*”。
重复单击左侧“Step”按钮(图4蓝色椭圆标注),直到右侧图中所有结点访问完毕。
记录访问的结点顺序,并与自己预测的深度优先、宽度优先、A*的搜索顺序比较。
(注:
运行过程中红色的结点保存于closed表中,绿色的结点保存于open表中,蓝色的结点表示新展开的结点)
6.单击主菜单中“File”→“CreatenewGraph”,激活左侧“create”卡,新建一个图。
分别选定“DepthFirst”、“BreadthFirst”、“A*”,进行搜索,记录搜索顺序,并与自己预测的深度优先、宽度优先、A*的搜索顺序比较。
实验二传教士和野人问题
【实验目的】
通过具体问题的编程求解,了解人工智能的基本解决方法;使用一种搜索策略,以加深理解。
【实验内容】
设在河的一岸有三个野人、三个传教士和一条船,要用这条船把所有的人运到河对岸,但受以下条件的约束:
(1)传教士和野人都会划船,但每次船上至多可载两个人;
(2)二是在河的任一岸,如果野人数目超过修道士数,传教士会被野人吃掉。
(3)如果野人会服从任何一次过河安排
编程实现一个确保传教士和野人都能过河,且没有传教士被野人吃掉的安全过河计划。
【实验原理】
传教士和野人问题是一个经典的智力游戏问题。
有N个传教士和N个野人来到河边准备渡河,河岸(甲岸和乙岸)有一条船,每次至多可供k人乘渡。
应如何规划摆渡方案,使得任何时刻,在河的两岸以及船上的野人数目总是不超过传教士的数目。
即求解传教士和野人从左岸全部摆渡到右岸的过程中,任何时刻满足M(传教士数)≥C(野人数)和M+C≤k的摆渡方案。
设N=3,k=2,则给定的问题可用下图表示,图中L和R表示左岸和右岸,B=1或0分别表示有船或无船。
约束条件是:
两岸上M≥C,船上M+C≤2
简单的分析,总共有10种可能的操作:
两个野人过河(从甲到乙,从乙到甲)
两个传教士过河从(从甲到乙,从乙到甲)
一个传教士和一个野人过河(从甲到乙,从乙到甲)
一个野人过河(从甲到乙,从乙到甲)
一个传教士过河(从甲到乙,从乙到甲)
但对于一个具体状态,只有5种可能的操作(从甲到乙或从乙到甲)。
先来看看问题的初始状态和目标状态,假设和分为甲岸和乙岸:
初始状态:
甲岸,3野人,3牧师;
乙岸,0野人,0牧师;
船停在甲岸,船上有0个人;
目标状态:
甲岸,0野人,0牧师;
乙岸,3野人,3牧师;
船停在乙岸,船上有0个人;
整个问题就抽象成了怎样从初始状态经中间的一系列状态达到目标状态。
问题状态的改变是通过划船渡河来引发的,所以合理的渡河操作就成了通常所说的算符,根据题目要求,可以得出以下5个算符(按照渡船方向的不同,也可以理解为10个算符):
渡1野人、渡1牧师、渡1野人1牧师、渡2野人、渡2牧师
算符知道以后,剩下的核心问题就是搜索方法了,本文以深度优先搜索为基础,通过一个FindNext(…)函数找出下一步可以进行的渡河操作,如果没有找到则返回其父节点,看看是否有其它兄弟节点可以扩展,然后用Process(…)函数递规调用FindNext(…),一级一级的向后扩展。
搜索中的算符采用下面的规则固定排序,合理的排序能更快的搜索到较优的解:
甲岸一次运走的人越多越好(即甲岸运多人优先),同时野人优先运走;乙岸一次运走的人越少越好(即乙岸运少人优先),同时牧师优先运走;
同时注意下面两点:
1.不能重复上次渡船操作(通过链表中前一操作比较),避免进入死循环;
2.任何时候河两边的野人和牧师数均分别大于等于0且小于等于3;
【实验过程】
设计程序求解传教士和野人问题。
附录C++程序及注释如下:
//wildman.cpp
#include"iostream.h"
typedefstruct_riverside//岸边状态类型
{
intwildMan;//野人数
intchurchMan;//牧师数
}RIVERSIDE;
typedefstruct_boat//船的状态类型
{
intwildMan;//野人数
intchurchMan;//牧师数
}BOAT;
typedefstruct_question//整个问题状态
{
RIVERSIDEriverSide1;//甲岸
RIVERSIDEriverSide2;//乙岸
intside;//船的位置,甲岸为-1,乙岸为1
BOATboat;//船的状态
_question*pPrev;//指向前一渡船操作
_question*pNext;//指向后一渡船操作
}QUESTION;
//函数定义
boolProcess(QUESTION*pQuest);
boolFindNext(QUESTION*pQuest);
voidVisitList(QUESTION*pQuest);//遍历链表输出结果
//
intmain()
{
//初始化
QUESTION*pHead=newQUESTION;
pHead->riverSide1.wildMan=3;
pHead->riverSide1.churchMan=3;
pHead->riverSide2.wildMan=0;
pHead->riverSide2.churchMan=0;
pHead->side=-1;//船在甲岸
pHead->pPrev=NULL;
pHead->pNext=NULL;
pHead->boat.wildMan=0;
pHead->boat.churchMan=0;
if(Process(pHead))
{
cout<<"成功渡河!
"<VisitList(pHead);
}
else
cout<<"到底怎样才能渡河呢?
郁闷!
"<//回收内存空间
while(pHead)
{
QUESTION*pTemp=pHead->pNext;
deletepHead;
pHead=pTemp;
}
pHead=NULL;
return0;
}
//渡船过程,递归调用函数FindNext(...)
boolProcess(QUESTION*pQuest)
{
if(FindNext(pQuest))
{
QUESTION*pNew=newQUESTION;
pNew->riverSide1.wildMan=pQuest->riverSide1.wildMan+pQuest->boat.wildMan*(pQuest->side);
pNew->riverSide1.churchMan=pQuest->riverSide1.churchMan+pQuest->boat.churchMan*(pQuest->side);
pNew->riverSide2.wildMan=3-pNew->riverSide1.wildMan;
pNew->riverSide2.churchMan=3-pNew->riverSide1.churchMan;
pNew->side=(-1)*pQuest->side;
pNew->pPrev=pQuest;
pNew->pNext=NULL;
pNew->boat.wildMan=0;
pNew->boat.churchMan=0;
pQuest->pNext=pNew;
if(pNew->riverSide2.wildMan==3&&pNew->riverSide2.churchMan==3)
returntrue;//完成
returnProcess(pNew);
}
else
{
QUESTION*pPrev=pQuest->pPrev;
if(pPrev==NULL)
returnfalse;//无解
deletepQuest;
pPrev->pNext=NULL;
returnProcess(pPrev);//返回其父节点重新再找
}
returntrue;
}
//判断有否下一个渡河操作,即根据比较算符找出可以接近目标节点的操作
//算符共5个:
1野/1牧/1野1牧/2野/2牧
boolFindNext(QUESTION*pQuest)
{
//基本规则
//渡船的优先顺序:
甲岸运多人优先,野人优先;乙岸运少人优先,牧师优先.
staticBOATboatState[5];//5个算符
if(pQuest->side==-1)
{
boatState[0].wildMan=2;
boatState[0].churchMan=0;
boatState[1].wildMan=1;
boatState[1].churchMan=1;
boatState[2].wildMan=0;
boatState[2].churchMan=2;
boatState[3].wildMan=1;
boatState[3].churchMan=0;
boatState[4].wildMan=0;
boatState[4].churchMan=1;
}
else
{
boatState[0].wildMan=0;
boatState[0].churchMan=1;
boatState[1].wildMan=1;
boatState[1].churchMan=0;
boatState[2].wildMan=0;
boatState[2].churchMan=2;
boatState[3].wildMan=1;
boatState[3].churchMan=1;
boatState[4].wildMan=2;
boatState[4].churchMan=0;
}
inti;//用来控制算符
if(pQuest->boat.wildMan==0&&pQuest->boat.churchMan==0)
//初始状态,第一次渡河时
i=0;//取算符1
else
{
for(i=0;i<5;i++)//扩展同一节点时,已用过的算符不再用,按优先级来
if(pQuest->boat.wildMan==boatState[i].wildMan&&pQuest->boat.churchMan==boatState[i].churchMan)
break;
i++;
}
if(i<5)
{
intj;
for(j=i;j<5;j++)
{
intnWildMan1=pQuest->riverSide1.wildMan+boatState[j].wildMan*pQuest->side;
intnChurchMan1=pQuest->riverSide1.churchMan+boatState[j].churchMan*pQuest->side;
intnWildMan2=3-nWildMan1;
intnChurchMan2=3-nChurchMan1;
//判断本次操作的安全性,即牧师数量>=野人或牧师数为0
if((nWildMan1<=nChurchMan1||nChurchMan1==0)&&
(nWildMan2<=nChurchMan2||nChurchMan2==0)&&
nWildMan1>=0&&nChurchMan1>=0&&nWildMan2>=0&&nChurchMan2>=0)
{
//本操作是否重复上次操作,注意方向不同
if(pQuest->pPrev!
=NULL)
{
if(pQuest->pPrev->boat.wildMan==boatState[j].wildMan&&
pQuest->pPrev->boat.churchMan==boatState[j].churchMan)
continue;
}
break;//该操作可行,推出循环
}
}
if(j<5)
{
pQuest->boat.wildMan=boatState[j].wildMan;
pQuest->boat.churchMan=boatState[j].churchMan;
returntrue;
}
}
returnfalse;
}
//遍历链表输出结果
voidVisitList(QUESTION*pQuest)
{
QUESTION*HD=pQuest;
cout<<"甲岸[野人,传教士]数量的变化:
"<while(HD!
=NULL)
{
cout<<"["<<(HD->riverSide1).wildMan<<","<riverSide1.churchMan<<"]";
if(HD->pNext!
=NULL)
cout<<"-->";
HD=HD->pNext;
}
cout<}
实验三八数码问题
【实验目的】
启发式搜索就是利用知识来引导搜索,达到减少搜索范围,降低问题复杂度的目的。
本实验通过解决八数码问题,帮助学生更好的熟悉和掌握启发式搜索的定义、估价函数和算法过程。
【实验内容】
在3×3的方格棋盘上,分别放置了表有数字1、2、3、4、5、6、7、8的八张牌,初始状态S0,目标状态Sg,如下图所示。
可以使用的操作有
空格左移,空格上移,空格右移,空格下移
即只允许把位于空格左、上、右、下方的牌移入空格。
请编程实现该问题的解决。
【实验原理】
一、搜索策略简介
1.无信息的搜索策略——盲目搜索
在不具有对特定问题的任何有关信息的条件下,按固定的步骤(依次或随即调用操作算子)进行的搜索,它能快速地运用一个操作算子。
盲目搜索中,由于没有可参考的信息,因此只要能匹配的操作算子都须运用,这会搜索更多的状态。
2.有信息的搜索——启发式搜索
考虑特定问题领域可应用的知识,动态地确定调用操作算子的步骤,优先选取较合适的操作算子,尽量减少不必要的搜索,以求尽快地到达结束状态,提高搜索效率。
启发式搜索每使用一个操作算子便需做更多的计算与判断,其性能一般要优于盲目搜索,但不可过于追求更多的甚至完整的启发信息。
二、搜索算法
1.辅助用的数据结构
open表:
记录已生成但其子状态未扩展的状态。
closed表:
记录其子状态已被扩展的状态。
(1)广度优先搜索算法中的open表采用的是先进先出的“队列”。
(2)深度优先搜索算法中的open表采用的是后进先出的“栈”。
(3)启发式搜索算法中的open表中的元素是按启发估价函数值的大小排列的一个表,每次从表中优先取出启发估价函数值最小的状态加以扩展。
同时并不丢弃其它的状态,而把它们保留在open表中。
2.算法过程
Procedurebreadth_first_search
Begin
Open:
=[start];closed:
=[]; {*初始化*}
Whileopen≠[]do
Begin
从open表中删除第一个状态,称之为n;
将n放入closed表中;
Ifn=目的状态ThenReturn(success);
生成n的所有子状态;
从n的子状态中删除已在open或closed表中出现的状态;{*避免循环搜索*}
将n的其余子状态,由不同的算法按不同的顺序加入到open表;
End;
End.
三、启发式策略中估价函数的确定
启发式策略中的“启发”(heuristic)可以体现在三个方面:
问题的表达;操作算子的构造;操作算子的选择。
这里特别关注操作算子的选择——控制性启发信息。
控制性启发信息往往被反映在估价函数之中。
估价函数的任务就是估计待搜索结点的“有希望”程度(或者说估计操作算子的“性能”),并依此给它们排定次序。
估价一个结点的价值,必须综合考虑两方面的因素:
已经付出的代价(实际);
将要付出的代价(估计)
f(n)=g(n)+h(n)
g(n)项保持了搜索的宽度优先成分,有利于搜索的完备性,但会影响搜索的效率,h(n)体现了搜索的启发信息,有利于搜索的效率,但影响搜索的完备性。
定义h*(n)为状态n到目的状态的最优路径的代价。
对一具体问题,只要有解,则一定存在h*(n)。
于是,当要求估价函数f(n)中的h(n)都小于等于h*(n)即
h(n)≤h*(n)时,A搜索算法就成为A*搜索算法。
【例】八数码问题的搜索树
初始S0
283123
164—>84
75765
Sg(0)
其估价函数可以定义为:
f(n)=d(n)+w(n)
(1)
其中d(n)代表状态的深度,每步为单位代价;w(n)表示以“不在位”的将牌数。
估价函数的另一个定义为:
f(n)=d(n)+p(n)
(2)
其中p(n)=将牌“不在位”的距离和。
采用这两种估价函数的A算法都是A*算法。
附录程序采用的估价函数为
(2)式,但其d(n)=0。
【实验过程】
估价函数:
////////////////////////////////////
//CaculateH:
计算当前H值
//参数:
当前节点
//返回值:
H值
////////////////////////////////////
intCMain:
:
CaculateH(CDisplay*Item)
{
DataTypeSrc[MaxItem][MaxItem];
for(inti=0;ifor(intj=0;jSrc[i][j]=Item->GetDispData(i,j);//数组Src为当前状态
intHop=0;
for(i=0;ifor(intj=0;