写在开头
这是"人工智能导论"课程的结课作业,里面包括了宽度优先搜索策略和全局择优搜索策略的算法描述与实现,并对于启发式函数进行了多次对比实验,主要介绍了6种可行的启发式函数,希望能给大家带来一些帮助.
项目源码见我的GitHub:https://github.com/Jupiter2Pluto/Eight-digit-problem
原文见:https://blog.csdn.net/qq_43393963/article/details/105848728
一、 问题描述
1.1 问题引入
对任意的八数码问题,给出求解结果。例如对于如下具体八数码问题:
通过设计启发函数,编程实现求解过程,如果问题有解,给出数码移动过程,否则,报告问题无解。
1.2 求解步骤
步骤一 设计八数码格局的隐式存储的节点结构
将表示棋局的状态用如下向量表示:
- A=(X0,X1 ,X2 ,X3 ,X4 ,X5 , X6 , X7 ,X8)
约束条件:
- Xi∈{0,1 ,2,3,4,5,6,7,8}(Xi≠Xj,当i≠j时)
初始状态:
- S0 =(1,3,2,4,0,5,6,7,8)
目标状态:
- Sg =(1,2,3,8,0,4,7,6,5)
步骤二 计算两个节点之间的可达性
(1)可以通过限定时间阈值或步骤阈值,判定两个节点之间的可达性。
(2)通过计算八数码节点的逆序数判断。如果一对数的前后位置与大小顺序相反,即前面的数大于后面的数,那么它们就称为一个逆序。一个排列中逆序的总数就称为这个排列的逆序数。逆序数为偶数的排列称为偶排列;逆序数为奇数的排列称为奇排列。如2431中,21,43,41,31是逆序,逆序数是4,为偶排列。
计算八数码节点的逆序数时将代表空格的0去除,例如
初始状态排列为:(1,3,2,4,5,6,7,8)
逆序数为:0+1+0+0+0+0+0+0=1即为奇排列
目标状态排列为:(1,2,3,8,4,7,6,5)
逆序数为:0+0+0+4+0+2+1+0=7即为奇排列
具有同奇或同偶排列的八数码才能移动可达,否则不可达。
步骤三 设计估计函数与启发函数
估计函数f(n)定义为:f(n)=d(n)+h(n) 其中,d(n)表示节点深度。
启发函数h(n)可参考如下定义方法:
(1)启发函数h(n)定义为当前节点与目标节点差异的度量:即当前节点与目标节点格局相比,位置不符的数字个数。
(2)启发函数h(n)定义为当前节点与目标节点距离的度量:当前节点与目标节点格局相比,位置不符的数字移动到目标节点中对应位置的最短距离之和。
(3)启发函数h(n)定义为每一对逆序数字乘以一个倍数。
(4)为克服了仅计算数字逆序数字数目策略的局限,启发函数h(n)定义为位置不符数字个数的总和与3倍数字逆序数目相加。
步骤四 选择并设计搜索算法(至少选择1个)
(1)使用盲目搜索中的宽度优先搜索算法。
(2)使用启发式搜索中的全局择优搜索算法。
(3)使用A*算法。
步骤五 设计输入输出
输入:
初始节点
目标节点
格式:
1 3 2 4 0 5 6 7 8
1 2 3 8 0 4 7 6 5
可采取以下三种方式输入:命令行、文件start.txt、GUI输入。
输出:如果无解在屏幕输出"目标状态不可达";如果有解请在屏幕输出"最少移动n步到达目标状态",n为最少移动的步骤数,并记录从初始状态到目标状态的每一步中间状态,并将这些状态保存至result.txt文件中。
以上过程可采取以下三种方式输出:命令行、文件result.txt、GUI输出。
步骤六 编写代码,调试程序
至少给出5组初始节点和目标节点对,并记录程序运算结果。
节点对要包括以下三种情况:不可达;最少移动步骤数≥10;最少移动步骤数≤6。
1.3 拓展实验(可任选1个完成)
-
记录步骤四中3个搜索算法的执行时间,并比较3者的效率。
-
在启发式搜索中,分别采用步骤三中启发式函数(1)(2)(3)(4),并比较四者的效率,思考如何进一步改进启发式函数。
-
试分析在所有可能的初始状态和目标状态之间最长的最小移动步骤数是多少。
二、 求解算法设计
2.1 全局择优搜索算法
(1) 把初始节点S0放入OPEN表,f(S0);
(2) 如果OPEN表为空,则问题无解,退出;
(3) 把OPEN表的第一个节点(记为节点n)取出放入CLOSED表;
(4) 考察节点n是否为目标节点:若是,则求得问题的解,退出;
(5) 若节点n不可扩展,则转第(2)步;
(6) 扩展节点n,用估计函数f(x)计算每个子节点的估价值,并为每个子节点配置指向父节点的指针;把这些子节点都送入OPEN表中,然后对OPEN表中的全部节点按估价值从小到大的顺序进行排序,然后转第(2)步。
2.2 节点扩展算法
(1) 取该节点八数码为0的数组下标;
(2) 如果0可以向左移动且父节点不是通过0右移得到的,就左移0生成新的子节点(以防止生成重复节点,下同);
(3) 如果0可以向上移动且父节点不是通过0下移得到的,就上移0生成新的子节点;
(4) 如果0可以向右移动且父节点不是通过0左移得到的,就右移0生成新的子节点;
(5) 如果0可以向下移动且父节点不是通过0上移得到的,就下移0生成新的子节点;
2.3 曼哈顿距离求解算法
在八数码问题中,某个节点的八数码与目标节点位置不符的某一个数字移动到目标节点中对应位置的最短距离,即为曼哈顿距离,其求解算法如下:
(1) 取该节点八数码与目标节点位置不符的某一个数字的数组下标indexA,取该数字在目标节点的数组下标indexB,最终移动步数result=0;
(2) 如果indexA > indexB,则交换两者的值;
(3) 计算两者的差differenceValue。
如果differenceValue≥3,result=1+(indexA+3与indexB的曼哈顿距离);
如果differenceValue=2,result=2;
如果differenceValue=0,result=0;
如果以上均不符合,那么计算(indexA+1)模3的值,如果为0,result取3,否则取1;
(4) 返回result的值。
三、 求解算法实现
3.1 数据结构设计
1.节点结构
struct State{
int index; // 状态号
int zeroIndex;//0的下标
vector<int>vec; // 8数码向量
double func;//估计函数
int depth;//深度
int parentIndex;//父节点下标
int direction;//由父节点如何移动得到:1(左)、2(上)、3(右)、4(下)
};
在八数码问题中,八数码的状态空间表示在程序中定义为一个结构体,记录该状态的各种信息:状态号、空格下标、八数码排列、估计函数值、深度、父节点下标(状态号)和父节点移动方向。
2.其它数据结构
list<State>OPEN;//open表
list<State>CLOSED;//closed表
vector<State>allState; // 全局所有的状态
vector<State>pathOfSolution; // 解路径
OPEN表和CLOSED表采用链表结构,OPEN表用于待考察的节点,CLOSED表用于存放已考察的节点;allState用于存放所有扩展的历史状态节点;pathOfSolution用于存放解路径。
3.2 核心算法实现
3.2.1 全局择优搜索算法实现
全局择优搜索算法是启发式搜索的一种,其主要思想是根据已知状态得到启发信息,根据启发信息辅助进行下一步搜索。其效率的高低主要与启发式函数的选择有关,代码实现过程将启发式函数封装,在该搜索算法中直接调用。实现代码如下:
void globalOptimal()
{
funcValue(s0);//计算s0的启发式函数值
OPEN.push_back(s0);
while (!OPEN.empty()) {
State maxFunc = OPEN.front();//OPEN表第一个节点就是希望最大的(估计函数值最小)
OPEN.pop_front();
CLOSED.push_back(maxFunc);
if (isTarget(maxFunc.vec)) { //判断是否是目标节点
int pathIndex = maxFunc.index;
do {
pathOfSolution.insert(pathOfSolution.begin(), allState[pathIndex]);
pathIndex = allState[pathIndex].parentIndex;
} while (pathIndex != 0);
pathOfSolution.insert(pathOfSolution.begin(), allState[0]);
return;
}
int nowStateNum = allState.size();
extendState(maxFunc);//扩展节点
// 如果扩展后allState数量不变,则无法扩展
if (nowStateNum == allState.size()) {continue; }
for (int i = nowStateNum; i < allState.size(); i++) {
OPEN.push_back(allState[i]);
}
OPEN.sort(riseCmp); //对OPEN表排序
}
return;
}
3.2.2 节点扩展算法实现
节点的扩展在该问题中极为重要,任何一种利用状态空间的搜索算法都需要该操作算子,其目的就是将空格(也就是八数码数组中的0元素)进行移动得到新的节点,该节点就是原来节点的子节点。另外该节点的direction表示该节点的父节点的0通过向哪个方向移动得到:1(左)、2(上)、3(右)、4(下)。
该算法的实现代码如下:
void extendState(State& s)
{
int zeroIndex = s.zeroIndex;//取该节点的0的下标
if (zeroIndex - 1 >= 0&&(zeroIndex%3!=0)) {//0向左移动
//如果父节点不是右移得到的,则子节点才能向左移,并生成新节点,下同
if(s.direction!=3) moveZero(s, 1);}
if (zeroIndex - 3 >= 0) {//向上
if (s.direction != 4) moveZero(s, 2);}
if (zeroIndex + 1 < s.vec.size() && ((zeroIndex+1) % 3 != 0)) {//向右
if (s.direction != 1) moveZero(s, 3);}
if (zeroIndex + 3 < s.vec.size()) {//向下
if (s.direction != 2) moveZero(s, 4);}
}
3.2.3 曼哈顿距离求解算法实现
在八数码问题中,某个节点的八数码与目标节点位置不符的某一个数字移动到目标节点中对应位置的最短距离,即为曼哈顿距离,实现代码如下:
int minMove(int indexA, int indexB)
{
if (indexA > indexB) {
int temp = indexA;
indexA = indexB;
indexB = temp; }
int result = 0;//最终移动步数
int differenceValue = indexB - indexA; // 下标的差值
if (differenceValue >= 3) {//如果大于3,即下移(因为一定不在一行)
result = 1 + minMove(indexA + 3, indexB);//①
}
else if (differenceValue == 2)
result = 2;//差值等于2,一定只需要移动2步
else if (differenceValue == 0)
result = 0;//差值等于0,不需要移动
else
result = (indexA + 1) % 3 == 0 ? 3 : 1;//②
return result;
}
在代码中有两处标号,在此说明:
①用递归的思想求解,因为最坏的情况是某个数字的下标需要从0移动到8;
②执行该语句时differenceValue必为1,为方便描述,数组下标在八数码棋盘上的分布如图3.2.1所示。有2种情况:(i)在同一行,如从1移动到2、从3移动到4等,只需要移动一步,这时indexA(相对于目标位置较小的下标如上面例子的1、3)加1后模3不为0;(ii)不在同一行,如从2移动到3、从5移动到6,需要移动3步,这时indexA(相对于目标位置较小的下标如上面例子的1、3)加1后模3为0。
四、 拓展实验完成情况
本项目完成了第二个拓展实验,主要完成了如下工作:
- 实现了4个不同的启发式函数进行全局择优搜索并对启发式函数进行改进。(详见4.1节)
- 重新定义了2种新的启发式函数并实现。(详见4.2节)
- 在第二个拓展实验外,完成了盲目搜索中的宽度优先搜索的实现。(详见4.3节)
4.1 四种启发式函数的实现与改进
4.1.1启发函数h(n)定义为当前节点与目标节点差异的度量
该启发式函数定义为当前节点与目标节点格局相比,位置不符的数字个数。实现代码如下:
double heuristicFunc1(State& s){
int funcValue = 0;
for (int i = 0; i < target.size(); i++) {
if (s.vec[i] != target[i]) {(target[i] == 0) ? funcValue : ++funcValue; }
}
return funcValue;
}
4.1.2 启发函数h(n)定义为当前节点与目标节点距离的度量
该启发式函数定义为当前节点与目标节点格局相比,位置不符的数字移动到目标节点中对应位置的最短距离之和。实现代码如下:
double heuristicFunc2(State& s)
{
int distance = 0;//最终距离
for (int i = 0; i < s.vec.size(); i++) {
if ((s.vec[i] != target[i]) && (s.vec[i] != 0)) {
distance += minMove(i, findVecIndex(target, s.vec[i])); }
}
return distance;
}
4.1.3 启发函数h(n)定义为每一对逆序数字乘以一个倍数
h(n)定义为每一对逆序数字乘以一个倍数,然后计算该状态与目标状态的差值,这个倍数相当于一个权重,一般在2到10之间取值,h(n)=|该状态逆序数个数-目标状态逆序数个数|*权重
。实现代码如下:
double heuristicFunc3(State& s,int weight)
{
return weight*abs(getInverseNum(s.vec) - getInverseNum(target));
}
改进方法:对权重重新定义,定义为逆序数之差。如:3 1,其权重即为3-1=2。即h(n)=|该状态逆序数权重之和-目标状态逆序数权重之和|
,两者差值越小,证明该状态与目标状态越接近。实现代码如下:
double heuristicFunc3(State& s)
{
int inverseNum = 0;
int targetInverseNum = 0;
for (int i = 0; i < s.vec.size() - 1; i++) {
for (int j = i + 1; j < s.vec.size(); j++)
{
if (s.vec[i] > s.vec[j] && s.vec[i] && s.vec[j])
inverseNum=inverseNum+ s.vec[i] - s.vec[j];
if (target[i] > target[j] && target[i] && target[j])
targetInverseNum = targetInverseNum + target[i] - target[j];
}
}
return abs(inverseNum-targetInverseNum);
}
4.1.4启发函数h(n)定义为位置不符数字个数的总和与3倍数字逆序数目相加
为克服了仅计算数字逆序数字数目策略的局限,启发函数h(n)定义为位置不符数字个数的总和与3倍数字逆序数目相加,即h(n)=错位数+3*|该状态数字逆序数目-目标状态数字逆序数目|
。实现代码如下:
double heuristicFunc4(State& s)
{
double differentNum = heuristicFunc1(s);
double result = 3 * abs(getInverseNum(s.vec) - getInverseNum(target)) + differentNum;
return result;
}
4.2 两种新的启发式函数并实现
4.2.1 启发函数h(n)定义为错位数与相同逆序数对数之差
受到第4个启发式函数的启发,可以同时考察错位数和逆序数,对于逆序数,求取该状态与目标状态的相同的逆序数的数量之差,差值越小,说明该状态与目标状态越接近。实现代码如下:
double heuristicFunc5(State& s)
{
int count = 0;
for (int i = 0; i < s.vec.size() - 1; i++) {
for (int j = i + 1; j < s.vec.size(); j++)
{
if (s.vec[i] > s.vec[j] && s.vec[i] && s.vec[j]) {
if (findVecIndex(target, s.vec[i]) < findVecIndex(target, s.vec[j]))
count++;} }
}
return heuristicFunc1(s)- count ;
}
4.2.2 启发函数h(n)定义为错位的数字移动到目标位的直线距离之和
只考查错位数字的下标与目标位置的下标之差,作为错位的数字移动到目标位的直线距离,求出所有距离的和值,结果说明该状态与目标状态越接近。虽然该方法忽略了八数码棋盘的结构,但要优于只考虑错位数的第一种启发式函数,因此可以作为一个参考。实现代码如下:
double heuristicFunc6(State& s)
{
int funcValue = 0;
for (int i = 0; i < target.size(); i++) {
if (s.vec[i] != target[i] && s.vec[i]&& target[i]) {
funcValue += abs(i - findVecIndex(target, s.vec[i]));}
}
return funcValue;
}
4.3 宽度优先搜索
宽度优先搜索算法是一种盲目搜索,从初始节点开始,不断扩展并将子节点加入OPEN表,直到OPEN表的第一个为目标节点。但效率较低,改进方法是:考察OPEN表的节点,如果(可扩展情况下)扩展该节点,其子节点中含有目标节点则成功返回。其算法描述如下:
(1) 把初始节点S0放入OPEN表;
(2) 初始节点是目标节点?若是,退出;若不是,下一步;
(3) OPEN表为空?若空,失败退出;
(4) 把OPEN表的第一个节点(记为节点n)取出放入CLOSED表;
(5) 扩展节点n,为每个子节点配置指向父节点的指针,把这些子节点都送入OPEN表中;
(6) 有子节点是目标节点?若有,成功退出;若没有,转(3)。
根据上述算法,实现代码如下:
void widthFirst()
{
OPEN.push_back(s0); //初始状态入OPEN表
if (isTarget(s0.vec)) { //判断是否是目标节点
int pathIndex = s0.index;
do {//根据生成的目标节点回溯得到解路径
pathOfSolution.insert(pathOfSolution.begin(), allState[pathIndex]);
pathIndex = allState[pathIndex].parentIndex;
} while (pathIndex != 0);
pathOfSolution.insert(pathOfSolution.begin(), allState[0]);
return;
}
else {
do {
if (OPEN.empty()) {
cout << "With First Search Error!" << endl;
return;
}
else {//依次考察OPEN表的第一个节点
State maxFunc = OPEN.front();
OPEN.pop_front();
CLOSED.push_back(maxFunc);
int nowStateNum = allState.size();
extendState(maxFunc);//扩展子节点
// 如果扩展后allState数量不变,则无法扩展
if (nowStateNum == allState.size()) {continue;}
for (int i = nowStateNum; i < allState.size(); i++) {
if (isTarget(allState[i].vec)) { //判断是否是目标节点
int pathIndex = allState[i].index;
do {
pathOfSolution.insert(pathOfSolution.begin(), allState[pathIndex]);
pathIndex = allState[pathIndex].parentIndex;
} while (pathIndex != 0);
pathOfSolution.insert(pathOfSolution.begin(), allState[0]);
return;
}
OPEN.push_back(allState[i]);
}
}
} while (!OPEN.empty());
}
}
五、 实验对比结果分析
5.1 测试用例1下的结果对比
测试用例1如图5.1.1所示:
该测试用例移动步数≤4,用宽度优先搜索以及全局择优搜索得到的结果如下表5.1.1所示:
首先对于两种搜索策略,由于移动步数较少,时间上相差较小,但从扩展状态中可以看出,宽度优先搜索明显要扩展更多的状态才能找到目标,而全局择优在估计函数的辅助下能够向接近目标地方向上扩展状态,从而更快地找到目标。
5.2 测试用例2、3下的结果对比
测试用例2(该测试用例移动步数≥10)如图5.2.1所示:
测试用例3(该测试用例移动步数≥20)如图5.2.2所示:
5.2.1 宽度优先与全局择优的前两种启发式函数比较
在测试用例2和3下,将宽度优先与全局择优的前两种启发式函数进行比较,得到的结果如下表5.2.1和表5.2.2所示:
从实验结果可以看出:①当移动步数增加时,全局择优搜索算法的优势就能够体现出来了,无论在扩展状态上(空间)还是计算时间上,都较宽度优先有明显优势。②只考虑错位数的heuristicFunc1的实现虽然较为简单,但效果不如heuristicFunc2(曼哈顿距离)好。
5.2.2 第3种启发式函数(heuristicFunc3)的对比实验
该启发式函数h(n)定义为每一对逆序数字乘以一个倍数(权值),实验将权值从1到10进行结果对比,并将改进的差值权重(逆序数的差值作为该对逆序数的权值)进行对比,在测试用例2和3下结果如下表5.2.3和5.2.4所示:
由实验结果可见:①测试用例2下,将权重设为3时效果最好,而改进后的差值权重相对于权重为3的结果又有提升,但解路径并不是最优,只保证了扩展状态较少和用时较短。③测试用例3下,将权重设为4时效果最好,而改进的差值权重效果却下降,猜想原因可能是随着深度增加,差值权重的影响力相对于深度的影响力小,从而导致扩展了更多的状态。
为了验证③中的猜想,又单独对差值权重进行了几组对比实验如表5.2.5,结果如下表5.2.6所示:
实验结果看出,③中的猜想正确,但如何控制深度和差值权重的比重还有待进一步研究。
5.2.2 第4、5、6种启发式函数的对比实验
这三种启发式函数下,有可以控制不同的系数来观察搜索效果,如表5.2.7所示,结果如表5.2.8所示:
从实验结果可以看出:①错位数与逆序数的组合可以较好地寻找目标状态,而其系数会影响扩展状态的个数;②不考虑八数码棋盘的格局情况下,只考虑数组下标位置作为启发信息依旧能寻找目标解,且在移动步数较多时效果优于只考虑错位数的启发式函数。
六、 实验结论
从以上实验可以得出结论:
- 盲目优先搜索中的宽度优先搜索在移动步数较多情况下其缺点会更加明显,即不能“智能”地寻找目标解;
- 全局择优搜索算法中,第二种启发式函数(曼哈顿距离)在移动步数或多或少的情况下都能够有较好表现;
- 对于估价函数,其深度d(n)与启发式函数h(n)的比重也会影响搜索效率,如果能找到一种比重自适应的方法将会比目前的搜索效率有较大提升;
- 本实验在选取启发式函数的时候,只考虑了一个节点到目标节点的某种距离或差异的度量,而没有考虑一个节点在最佳路径上的概率,因此有部分启发式函数并不能很好地找到最优解。
七、 个人体会与总结
略略略~