前言
本实例是对第三章的总结与应用,分别利用无信息搜索和有信息搜索对八数码问题进行求解,加深对不同策略的特点的印象与理解。
1 问题描述
在九宫格里放在1到8共8个数字还有一个是空格,与空格相邻的数字可以移动到空格的位置,问给定随机的初始状态,需要几步能到达目标状态(用0表示空格),目标状态如图2所示。
![]() | ![]() |
- 状态空间:状态描述指明8个棋子以及空格在棋盘9个方格上的分布。
- 初始状态:任何状态都可能是初始状态,注意要到达任何一个给定的目标。
- 后继函数(行动空间+转移模型):用来产生通过四个行动(把空位向Left、Right、Up或Down移动)能够达到的合法状态。
- 目标测试:用来检测状态是否能匹配上图2中所示的目标布局。
- 路径耗散:每一步的耗散值为1,因此整个路径的耗散值是路径中的步数。
2 问题求解
2.0 代码的一些功能函数
- 关键结构体的定义
/*
*描述:声明一个节点,存储当前九宫格状态
* 以目标状态为例,puzzle: {{0,0,0},{0,1,1},{0,2,2},
{1,0,3},{1,1,4},{1,2,5},
{2,0,6},{2,1,7},{2,2,8},}
nextActionList: {[1,0] 向上移动
[-1,0] 向下移动
[0,1] 向左移动
[0.-1]} 向右移动
nextAxtionList的大小<=4
depth: 当前状态所处深度
*/
typedef struct {
//vector<PUZZLE> puzzles;
PUZZLE puzzle[9];
vector<vector<int>> nextActionList;//记录某一状态结点的可走动作序列
vector<vector<int>> precedeActionList;//记录之前走过的动作序列,便于输出
int depth;
int m_f1 = -1;//第一种启发式的评估函数
int m_f2 = -1;//第二种启发式的评估函数
} PUZZLE_NODE;
- 主要的功能函数
//更新puzzleNode的后继可操作动作状态
PUZZLE_NODE updatePuzzleNodeActionList(PUZZLE_NODE puzzleNode);
//给定动作action(action为二维数组)和puzzleNode,返回执行该动作后新的节点
PUZZLE_NODE moveToPuzzleNode(vector<int> action, PUZZLE_NODE puzzleNode);
//用于生成PuzzleNode中随机动作索引,用于随机后退,从目标状态随机回退,产生新的初始状态。
int getRandomNumber(int actionSize);
//给定回退步数,返回初始节点状态
PUZZLE_NODE initialPuzzleNode(int backwardSteps);
//判断当前节点状态和目标节点状态在同一位置上两个8数码状态是否相同。
bool isEqual(PUZZLE puzzle1, PUZZLE puzzle2);
//检测当前节点和目标节点状态是否相同。
bool checkObject(PUZZLE_NODE currentNode, PUZZLE_NODE objNode);
//用于输出走过的动作序列(从初始到目标的路径)
void outputAction(vector<int> action, int index);
//用于生成当前状态对应的唯一数字,用于eightFigureFramework中visited判断当前节点状态是否访问过。
int visitedNum(PUZZLE_NODE puzzleNode);
//用于计算每个状态的错位数(第一个启发式函数)
int SumofUnposition(PUZZLE_NODE puzzleNode);
//用于计算每个状态的曼哈顿距离之和(第二
个启发式函数)
int SumofManhattanDis(PUZZLE_NODE puzzleNode);
2.1 广度优先搜索
- 算法流程
利用广度优先搜索,每次扩展相邻的状态结点,利用队列先进先出的性质,将新扩展的结点放入队列中,然后在新的循环中从队列中取出头部结点,标记为已访问过结点后,判断是否为目标状态,若不是,则继续重复上述步骤,若是,输出结果。 - 关键代码展示
int* binaryFirstSearch(PUZZLE_NODE initialNode, PUZZLE_NODE objPuzzleNode)
{
int result[2] = { 0,0 };
cout << "初始节点状态:" << endl;
//打印初始状态
for (int i = 0; i < 3; i++)
{
cout << " " << initialNode.puzzle[i * 3 + 0].puzzleId << " " << initialNode.puzzle[i * 3 + 1].puzzleId <<
" " << initialNode.puzzle[i * 3 + 2].puzzleId << endl;
}
cout << endl;
PUZZLE_NODE puzzleNode = initialNode;
//定义一个结点队列,以便存放结点
queue<PUZZLE_NODE> puzzleNodeQueue;
//将初始结点深度设为0
puzzleNode.depth = 0;
//将初始结点存入队列中
puzzleNodeQueue.push(puzzleNode);
//当队列中无结点,退出循环,表示无解
while (puzzleNodeQueue.size())
{
//从队列头部取出结点
PUZZLE_NODE currentPuzzleNode = puzzleNodeQueue.front();
//若当前结点状态和目标状态一致,则打印正确结果;若不一致则继续扩展结点查找
if (checkObject(currentPuzzleNode, objPuzzleNode))
{
//将该节点的前继动作按顺序打印,得到并输出移动的步骤
for (int i = 0; i < currentPuzzleNode.precedeActionList.size(); i++)
{
outputAction(currentPuzzleNode.precedeActionList[i], i + 1);
}
cout << "找到正确结果:" << endl;
//打印出结点状态
for (int i = 0; i < 3; i++)
{
cout << " " << currentPuzzleNode.puzzle[i * 3 + 0].puzzleId << " " << currentPuzzleNode.puzzle[i * 3 + 1].puzzleId <<
" " << currentPuzzleNode.puzzle[i * 3 + 2].puzzleId << endl;
}
cout << endl;
result[0] = 1;
//返回深度即行走步数
result[1] = currentPuzzleNode.depth;
return result;
}
else
{
//将当前结点标记为已访问
visited[visitedNum(currentPuzzleNode)] = 1;
//若结点已更新过,则不重新更新结点的可走动作序列
if (currentPuzzleNode.nextActionList.size() == 0)
{
currentPuzzleNode = updatePuzzleNodeActionList(currentPuzzleNode);
}
//将当前结点pop出
puzzleNodeQueue.pop();
//循环动作序列,扩展可走相邻结点
for (int i = 0; i < currentPuzzleNode.nextActionList.size(); i++)
{
PUZZLE_NODE nextPuzzleNode = moveToPuzzleNode(currentPuzzleNode.nextActionList[i], currentPuzzleNode);
//将前继动作放入当前的nextPuzzleNode,便于移动步骤的回查
if (!currentPuzzleNode.precedeActionList.empty())
{
for (int actionIndex = 0; actionIndex < currentPuzzleNode.precedeActionList.size(); actionIndex++)
{
nextPuzzleNode.precedeActionList.push_back(currentPuzzleNode.precedeActionList[actionIndex]);
//把之前的状态走过的路copy在新的状态中
}
}
//接着把最新的动作同样存入当前最新的结点中
nextPuzzleNode.precedeActionList.push_back(currentPuzzleNode.nextActionList[i]);
//若该结点已访问过,则跳过循环
if (visited[visitedNum(nextPuzzleNode)] == 1)
{
continue;
}
//结点深度+1
nextPuzzleNode.depth = currentPuzzleNode.depth + 1;
//将可走结点放到队列中
puzzleNodeQueue.push(nextPuzzleNode);
}
}
}
return result;
}
2.2 深度有限搜索
- 算法流程
利用深度有限搜索,与深度优先类似,只是设置了一个深度上限,防止无限地扩展。
这里每次只扩展一个新的结点,而这个新的结点是从前继结点的可走动作序列中选择一个可走的动作进行更新的,更新后则需要将该新结点压入栈中。
在进入下一轮循环中,利用栈的后进先出性质,取出栈顶的结点,标为已访问后,同样先判断是否达到目标状态,若不是则在扩展新结点之前,需利用已访问的结点来更新当前结点的可走动作序列(保证每一次更新的状态都是有效的),重复上述步骤直至找到目标状态。 - 关键代码展示
int* depthFirstSearch(PUZZLE_NODE initialNode, PUZZLE_NODE objPuzzleNode)
{
int result[2] = { 0,0 };
cout << "初始节点状态:" << endl;
//打印初始状态
for (int i = 0; i < 3; i++)
{
cout << " " << initialNode.puzzle[i * 3 + 0].puzzleId << " " << initialNode.puzzle[i * 3 + 1].puzzleId <<
" " << initialNode.puzzle[i * 3 + 2].puzzleId << endl;
}
cout << endl;
PUZZLE_NODE puzzleNode = initialNode;
//定义一个结点栈,存放结点
stack<PUZZLE_NODE> puzzleNodeStack;
//初始化结点深度
puzzleNode.depth = 0;
//将初始状态结点压入栈中
puzzleNodeStack.push(puzzleNode);
while (puzzleNodeStack.size())
{
//从栈顶取出结点
PUZZLE_NODE currentPuzzleNode = puzzleNodeStack.top();
//若当前结点状态和目标状态一致,则打印正确结果;若不一致则继续扩展结点查找
if (checkObject(currentPuzzleNode, objPuzzleNode) && currentPuzzleNode.depth < MAX_DEPTH)
{
//判断是否达到目标状态后,将该节点的前继动作按顺序打印,得到并输出移动的步骤
//由于与前面代码有重复,不重复展示
}
else
{
//寻找相邻子节点并任意找一个结点放入stack中
visited[visitedNum(currentPuzzleNode)] = 1;
//更新该结点的可走动作
if (currentPuzzleNode.nextActionList.size() == 0)
{
currentPuzzleNode = updatePuzzleNodeActionList(currentPuzzleNode);
}
//遍历访问过的结点,对当前结点的可走动作序列进行剪枝
for (int i = 0; i < currentPuzzleNode.nextActionList.size(); i++)
{
PUZZLE_NODE t_nextPuzzleNode = moveToPuzzleNode(currentPuzzleNode.nextActionList[i], currentPuzzleNode);
if (visited[visitedNum(t_nextPuzzleNode)] == 1)
{
currentPuzzleNode.nextActionList.erase(currentPuzzleNode.nextActionList.begin() + i);
i = i - 1;//**注意vector容量的变化
}
}
//每次选择可走动作的第一个,若达到最深结点,需要将现有结点删除
if (currentPuzzleNode.depth< MAX_DEPTH)
{
//若现有结点动作序列不为零,则扩展新节点;若无,则直接将现有结点删除
if(currentPuzzleNode.nextActionList.size() != 0)
{
PUZZLE_NODE nextPuzzleNode = moveToPuzzleNode(currentPuzzleNode.nextActionList[0], currentPuzzleNode);
//将前继动作放入当前的nextPuzzleNode,便于移动步骤的回查
if (!currentPuzzleNode.precedeActionList.empty())
{
for (int actionIndex = 0; actionIndex < currentPuzzleNode.precedeActionList.size(); actionIndex++)
{
//把之前的状态走过的路copy在新的状态中
nextPuzzleNode.precedeActionList.push_back(currentPuzzleNode.precedeActionList[actionIndex]);
}
}
//接着把最新的动作同样存入当前最新的结点中
nextPuzzleNode.precedeActionList.push_back(currentPuzzleNode.nextActionList[0]);
//结点深度+1
nextPuzzleNode.depth = currentPuzzleNode.depth + 1;
//将可走的结点压入栈中
puzzleNodeStack.push(nextPuzzleNode);
}
else
{
//若无可走动作,则将当前结点pop出
puzzleNodeStack.pop();
}
}
else
{
//若达到最大搜索深度,则将当前结点pop出
puzzleNodeStack.pop();
}
}
}
return result;
}
2.3 启发式搜索:不在位的数码数或曼哈顿距离之和
- 算法流程
启发式搜索是一种有信息的搜索策略,与无信息搜索最大不同就是其在进行状态转移时需要评估可走状态到目标状态的距离(h(n)),这里的启发式用的A*搜索,与贪婪算法不同就是它的评估函数(f(n))需要加上行动的代价,保证了其最优性,之前的笔记也说过,多提一嘴。
扩展结点时与BFS类似,扩展该节点的所有可走的状态,重点就是这里需要一个open列表来放置待扩展的结点,那么真正执行动作的依据是什么呢?则需要比较该open列表中所有状态的启发式函数,取出最小的一个进行扩展,然后重复上述步骤,将扩展出来的结点放入open列表中,此时就继续比较(open列表此时有所有之前待扩展的结点),重复循环,直至找到目标状态。 - 关键代码展示
int* heuristicSearchInformedByIncorrectNum(PUZZLE_NODE initialNode, PUZZLE_NODE objPuzzleNode)
{
int result[2] = { 0,0 };
cout << "初始节点状态:" << endl;
//打印出初始状态
for (int i = 0; i < 3; i++)
{
cout << " " << initialNode.puzzle[i * 3 + 0].puzzleId << " " << initialNode.puzzle[i * 3 + 1].puzzleId <<
" " << initialNode.puzzle[i * 3 + 2].puzzleId << endl;
}
cout << endl;
PUZZLE_NODE puzzleNode = initialNode;
//初始化结点深度
puzzleNode.depth = 0;
int depth = 0;
//定义一个open链表,用于存放扩展结点
vector<PUZZLE_NODE> openList;
//将初始结点存入open链表
openList.push_back(puzzleNode);
//当链表不空则循环,直至找到目标状态;若空则跳出循环,无解
while (!openList.empty())
{
//从openlist中取出头部结点
PUZZLE_NODE currentPuzzleNode = openList.front();
//若当前结点状态和目标状态一致,则打印正确结果;若不一致则继续扩展结点查找
if (checkObject(currentPuzzleNode, objPuzzleNode))
{
//判断是否达到目标状态后,将该节点的前继动作按顺序打印,得到并输出移动的步骤
//由于与前面代码有重复,不重复展示
}
else
{
//将已访问过的结点标记为1
visited[visitedNum(currentPuzzleNode)] = 1;
if (currentPuzzleNode.nextActionList.size() == 0)
{
currentPuzzleNode = updatePuzzleNodeActionList(currentPuzzleNode);
}
//只对走过(从openlist pop出来)的结点进行剪枝
for (int i = 0; i < currentPuzzleNode.nextActionList.size(); i++)
{
PUZZLE_NODE t_nextPuzzleNode = moveToPuzzleNode(currentPuzzleNode.nextActionList[i], currentPuzzleNode);
if (visited[visitedNum(t_nextPuzzleNode)] == 1)
{
currentPuzzleNode.nextActionList.erase(currentPuzzleNode.nextActionList.begin() + i);
i = i - 1;//**注意vector容量的变化
}
}
//循环当前结点的可走动作序列扩张结点
for (int i = 0; i < currentPuzzleNode.nextActionList.size(); i++)
{
int t_h = 0;
int t_isExist = 0;
PUZZLE_NODE nextPuzzleNode = moveToPuzzleNode(currentPuzzleNode.nextActionList[i], currentPuzzleNode);
//若新扩展的结点已存在,则跳过
for (int i = 0; i < openList.size(); i++)
{
if (visitedNum(nextPuzzleNode) == visitedNum(openList[i]))
{
t_isExist = 1;
break;
}
}
if (t_isExist)
{
continue;
}
//把新节点的前继动作加上
if (!currentPuzzleNode.precedeActionList.empty())
{
for (int actionIndex = 0; actionIndex < currentPuzzleNode.precedeActionList.size(); actionIndex++)
{
//把之前的状态走过的路copy在新的状态中
nextPuzzleNode.precedeActionList.push_back(currentPuzzleNode.precedeActionList[actionIndex]);
}
}
//接着把最新的动作同样存入当前最新的结点中
nextPuzzleNode.precedeActionList.push_back(currentPuzzleNode.nextActionList[i]);
//计算出结点深度
nextPuzzleNode.depth = currentPuzzleNode.depth + 1;
//计算出启发式函数值
nextPuzzleNode.m_f1 = nextPuzzleNode.depth + SumofUnposition(nextPuzzleNode);
openList.push_back(nextPuzzleNode);
}
//新节点扩展完后就要删除父节点
openList.erase(openList.begin());
int t_minh = -1;
int t_minNode = 0;
for (int i = 0; i < openList.size(); i++)
{
//对openlist里面的启发式进行排序,找最小
if (t_minh == -1)
{
t_minh = openList[i].m_f1;
t_minNode = i;
}
else if (openList[i].m_f1 < t_minh)
{
t_minh = openList[i].m_f1;
t_minNode = i;
}
}
//将启发式最小的结点放在openlist最前面
PUZZLE_NODE t_PuzzleNode = openList[t_minNode];
openList[t_minNode] = openList[0];
openList[0] = t_PuzzleNode;
}
}
return result;
}