八数码问题——给定随机生成的初始状态和如下的目标状态,分别实现 IDS (迭代深度搜索)、贪婪搜索以及A*搜索算法,找到一个从初始状态到目标状态的行动路径。
要求:
- A*算法至少需要实现 2种启发式函数
- 根据结果对比分析不同搜索算法的运行时间,注意返里的运行时间取多次不同随机初始状态的运行时间的平均结果。
先大概说一下这三种方法的思路:
- A*搜索:总代价 f(n) = g(n) + h(n),其中g(n)为从初始状态到达该状态的代价(这里一个状态也就是A*算法里常说的一个节点),h(n)为从当前状态到目标状态的预估代价。因此,该算法的思路就是,每次寻找总代价f(n)最小的点进行扩展,直到找到终点。A* 的具体过程可参考这篇博客,我就是看这篇博客看会的A* :https://blog.youkuaiyun.com/hitwhylz/article/details/23089415
- 贪心搜索:代价f(n) = h(n),每次只考虑(可到达的)离目标节点最近的点进行扩展
- IDS深度迭代搜索:在有界的深度优先搜索的基础上迭代的设置边界,即先考虑一层的DFS,若找不到目标则找两层的DFS,一直迭代下去直到找到目标节点。所以这种方法也是一定能找到界的,只是时间消耗会很多。
有人说,A*是的代价函数是考虑了g和h,贪心的代价是只考虑了h,那么只考虑g的是什么算法呢? 答案就是我们数据结构中学过的Dijkstra最短路算法。
- Dilkstra算法只考虑当点代价最小的点(f = g),所以需要扩展的节点是最多的,耗时是最长的,但是这种方法一定能找到最优解(也即是路径最短的解);
- 而贪心每次只考虑离目标节点最近的点(f = h),所以很容易很快就能找到目标节点,但是这种方法找到的解往往不是最优的(也就是找到的路径往往不是最短的);
- 那么,A*就是结合了二者的优点 (f = g + h),既考虑当前的代价,有考虑离目标点的预估代价,既保证了找到的解是最优的,又使得所搜速度相对于Dijkstra变快了,因此,A*算法是这几种方法中效率最高的完备(能找到最优解)的方法。
下面简单介绍一下代码思路:
A*算法
- 先生成随机排列的整数0~8
可以用C++的STL库函数random_suffle(temp.begin(), temp.end(),myrandom)来实现,第一第二个参数分别为vector的头迭代器和尾迭代器,然后第三个参数是自己定义的随机函数,这里设置随机数种子srand((int)time(0))便可以使每次生成的序列不一样。
// 随即生成函数
int myrandom (int i) {
return rand()%i;
}
//生成0~8的随机整数序列,作为开始状态
void randperm()
{
vector<char> temp;
for (int i = 0; i < 9; ++i) temp.push_back(i+'0'); //生成0~8
random_shuffle(temp.begin(), temp.end(),myrandom); //随机乱序
for(int i=0;i<3;i++){
for(int j=0;j<3;j++){
start.matrix[i][j] = temp[i*3+j];
}
}
}
- 检查生成的随机序列是否有解
我们知道,若两个不同的状态序列的逆序数同奇偶,则二状态可以互达,否则不能互达。逆序数就是对于每个数,前面比它大的数的个数的总和。我们的目标状态是123456780,那么它的逆序数是0(忽略0板块),是偶排列,那么我们可以计算初始状态的逆序数,如果是偶数,则问题可解;否则问题无解,从初始状态无论怎么移动都无法达到目标状态。
//判断是否有解
//初始状态的逆序数应与目标状态的同奇偶,目标状态为偶排列
bool check_if_solvable()
{
string start_seq = matrix2string(start.matrix);
int cnt=0;
for(int i=0;i<9;i++){
if(start_seq[i]=='0') continue;
for(int j=i-1;j>=0;j--){
if(start_seq[j]>start_seq[i]) cnt++;
}
}
if(cnt%2==0) return true;
return false;
}
- 若问题有解,那么迕入 A*算法
因为要进行多次重复实验,因此每次进入函数时应先将各个变量、各个容器清零,避免上次运行的结果对后面造成影响。同时,初始化初始节点,将初始节点放入open list中。
int AStar(State& start){
//清零
while(!openList.empty()) openList.pop();
openSeq.clear();
closeSeq.clear();
//给start节点的zero_x,zero_y赋值
find_blankTile();
start.g=0;
openList.push(start);
openSeq.insert(matrix2string(start.matrix));
然后遍历open list,每次都取f值最小的出来,因为是优先队列,所以取队首就好了。取出来先判断是否达到目标状态,若达到,则反向搜寻将路径记录下来并返回移动步数;
//遍历,直到open列表为空
while(!openList.empty()){
State *curState = new State(openList.top()); //当前状态。 因为是优先队列, 所以top出来的是f值最小的
//cout<<"round: "<<index++<<endl;
//cout<<"cur:"<<endl;
//print_matrix(curState->matrix);
//cout<<"parent"<<curState->parent<<endl;
//判断是否到达目标状态
if(matrix2string(curState->matrix)==matrix2string(goal.matrix)){
int step = curState->g;
/***
根据父节点一步步往回寻路,输出路径
*/
while(!stack_path.empty()) stack_path.pop(); //清空栈
State* p = curState;
while(p!=NULL){
stack_path.push(*p);
p = p->parent;
}
//返回所需步数
return step;
}
否则,沿当前节点(当前状态curState)扩展。因为每个节点(状态)是用结构体表示的,数字排列用二维数组存储,难以直接比较两个状态是否相同,因此这里我将二维数组转成字符串来对比两个序列是否一样,将每个访问过的节点的字符串序列存入到STL set中(即close list),便可以直接用find()函数查找改状态是否在close list中了。
这里代码的思路是这样的:首先将当前节点从openlist中移除并加入到close list中,代表该节点已访问过;然后将该节点向四个方向扩展,若某方向超出范围则跳过,若某方向的状态已访问过(即在close list中)也跳过,那么对于可扩展的点,将父节点设为curState、g值为curState.g加上这一步的代价(这里每一步的代价都为1),然后算得启发式函数值,也就是预估一下从该扩展节点到目标节点的代价,然后将当前代价g加上估算代价得到代价f,并将节点存入open list中。这里我没有判断该扩展节点是否已在开放列表中,按理说A*算法是需要判断的,若节点在开放列表的话,就需要判断这条路径(即从当前节点到达该扩展节点)的代价是否比它原来的代价小,这里只用比较g值就行了,因为同一点到目标节点的估算代价h用同一个启发式函数计算出来是一样的。因为优先队列无法进行查找和更新,且对于这个问题每一步的代价g都是只加1的,因此这里不判断也不会出现问题。
string nextSeq="";//下一个状态的字符串序列
for(int i=0;i<4;i++){ //四个交换方向,分别为上右下左
// cout<<"i: "<<i<<endl;
State* nextState = new State(*curState);//下一个状态.先把父节点的内容复制过来,之后再改特定内容
nextState->zero_x = curState->zero_x+move_x[i];
nextState->zero_y = curState->zero_y+move_y[i];
if(nextState->zero_x<0||nextState->zero_x>2||nextSta