之前遇到这个问题时没能找到好的答案,所以我在这里补充我最后使用的可行方法。
目录
解决思路
打乱后的图片通过白块向各个方向的移动来进行还原。我们将图像状态描述为节点,以三阶为例,8表示白块的位置,我们希望最终是(0,1,2,3,4,5,6,7,8)的有序序列。在图上画出该节点。
白块8可以向上和向左两个方向做移动,相当于玩家对当前节点的两种选择,走的两种不同的路径,将当前节点移动一步后能达到的所有可能在图上画出,新节点与当前节点的相对位置和白块移动方向一致,把这个过程称作对当前节点做拓展。拓展后的结果如下。
最初的节点拓展出了两个子节点,也就是玩家可能遇到的两种情况。对还未拓展的节点(黑色文字)进行拓展并重复:
继续拓展下去,生成一个图,但不是在完整的一张图上搜索,而是拓展一次生成一个结点,于是采用A*搜索算法实现寻找最短路径。
A*算法这里不做教学。
A搜索算法相比于深度和广度搜索的区别在与使用了评价函数来指导搜索,因而不是盲目搜索。评价函数为F(n)=G(n)+H(n),在本例中,G为初始节点到该节点白块移动的次数。因为移动方向的限制,迷宫中H(n)的计算通常采用计算当前节点与目标节点的曼哈顿距离,但在本例中无法直接得到目标节点的位置,不能通过路径计算当前节点和目标节点的曼哈顿距离。但是在状态节点中,每一个成员与它的目标位置(有序序列)都有一个曼哈顿距离,其总和与在路径中的曼哈顿距离一样可以估计当前节点到目标节点的距离。所以H(n)采用的是计算节点中所有成员与目标节点中该成员的曼哈顿距离总和。
算法过程
1.把起点加入 open list 。
2.重复如下过程:
a.遍历open list,查找F值最小的节点,把它作为当前要拓展的节点,然后移到close list中
b.对当前节点拓展后的节点一一进行检查,如果它是不可抵达的或者它在close list中,忽略它。否则,做如下操作: 如果它不在open list中,把它加入open list,并且把当前节点设置为它的父亲。
如果它已经在open ist中,检查这条路径(即经由当前方格到达它那里)是否更近。如果更近,把它的父亲设置为当前方格并重新计算它的G和F值。
c. 遇到下面情况停止搜索:
把终点加入到了 open list 中,此时路径已经找到了,或者
查找终点失败,并且open list 是空的,此时没有路径。
3.从终点开始,每个方格沿着父节点移动直至起点,形成路径。
运行结果
C代码
代码可以直接运行。由于随机打乱的九宫格有几率不能还原,所以加了一些函数来保证打乱后的九宫格是可以还原的。由于我自己使用时九宫格状态采用二维数组保存,所以代码中有些地方是对二维数组进行操作后再转为一维数组,不是故意想繁琐,我只是提供思路,所以对代码没有做过多修改。
代码不是完全原创,但是那位博主的代码功能并不能总是正常实现,而且运行效率较低,我做了一些修改。参考:https://www.codenong.com/cs105897291/
open,close表也可以采用动态数组。一些情况需要十几秒才能有结果,如果发现有哪种情况几分钟还没有运行出结果,请记下map数组与我反馈。
#include <stdio.h>
#include<time.h>
#include <stdlib.h>
#define MAXLISTSIZE 10000
#define MAXSTEPSIZE 100
//节点结构体
struct ENode
{
int status[9];//结点状态
int G;
int H;//启发函数中的H
int F;//评价函数中的F
int White;//白块位置,即‘8’的位置
int step;//step存储该节点是上一节点通过怎样的移动得到的(1左2右3上4下)
ENode* Parent;//父节点
};
//最终目标状态
int FinalStatus[9] = { 0, 1, 2, 3, 4, 5, 6, 7, 8 };
//定义OPEN表和CLOSE表,open和close是表中最后一个内容的下一位序号
ENode OPEN[MAXLISTSIZE];
ENode CLOSE[MAXLISTSIZE];
int open = 0;
int close = 0;
void initmap(int map[3][3])
{
int i, j;
int array[8] = { 0 };
for (i = 0; i <= 7; i++)
array[i] = i;
int length = sizeof(array) / sizeof(array[0]);
for (i = 0; i < 3; i++)
{
for (j = 0; j < 3; j++)
{
if (i == 2 && j == 2)
{
map[i][j] = 8;
return;
}
int pos = rand() % length;
map[i][j] = array[pos];
//调整一维数组
for (int k = pos; k < length; k++)
{
array[k] = array[k + 1];
}
length--;
}
}
}
//计算逆序数对数
int countInversions(int arr[], int n)
{
int inversions = 0;
for (int i = 0; i < n - 1; i++)
{
for (int j = i + 1; j < n; j++)
{
if (arr[i] > arr[j])
{
inversions++;
}
}
}
return inversions;
}
// 判断解的存在性,
int isSolvable(int map[3][3])
{
int* arr = new int[9];
int index = 0;
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
{
*(arr + index) = map[i][j];
index++;
}
int inversions = countInversions(arr, 9);
delete[] arr; // 释放数组空间
arr = NULL; // 将指针置为NULL
return (inversions % 2 == 0); // 如果逆序数的个数是偶数,则存在解
}
//求绝对值
int _abs(int a)
{
if (a < 0)
return (-a);
return a;
}
//计算H(n).时间复杂度为O(n)
int CountH(int* s)
{
int H = 0;
int i, distance = 0;
for (i = 0; i <= 8; i++)
{
if (s[i] == 8) continue;//白块不加入计算,少计算一次
distance = _abs(s[i] - i);
distance = distance / 3 + distance % 3;
H += distance;
}
return H;
}
//判断节点是在哪个表中,返回正值在open中,负值在close中,0不在两个表中.时间复杂度为O(9n)*2,n为当前open中的结点数
int Exist(ENode node)
{
int i, j;
int H = 0; //执行后H如果为0,则证明给函数的节点在表中已存在
for (i = 0; i <= open - 1; i++) //判断是否在OPEN表.
{
for (j = 0; j <= 8; j++)
{
if (node.status[j] != OPEN[i].status[j])
{
H++;
}
}
if (H == 0) //H=0证明在表中找到该节点
{
return i; //如果在OPEN表中,返回i,节点在OPEN的位置
}
H = 0; //扫描完一个节点后重置H
}
for (i = 0; i <= close - 1; i++) //判断是否在CLOSE表
{
for (j = 0; j <= 8; j++)
{
if (node.status[j] != CLOSE[i].status[j])
{
H++;
}
}
if (H == 0) //H=0证明在表中找到该节点
{
return (-i); //如果在CLOSE表中,返回-i(i为节点在CLOSE的位置)
}
H = 0; //扫描完一个节点后重置H
}
return 0;
}
//对OpEN进行排序
//查找插入位置
int OpenSearch(int x)
{
int mid;
int low = 0, high = open - 1;
while (low <= high)
{
mid = (low + high) / 2;
if (OPEN[mid].F == x)
return mid;//后来居上,即新加入的结点优先进行拓展
if (OPEN[mid].F < x)
low = mid + 1;
if (OPEN[mid].F > x)
high = mid - 1;
}
return low;
}
//将新节点插入OPEN表
void Insert(ENode newNode)
{
int index = OpenSearch(newNode.F);
for (int i = open - 1; i >= index; i--)
OPEN[i + 1] = OPEN[i];
OPEN[index] = newNode;
open++;
}
//初始化节点
void ENodeInit(ENode* Temp, int* status, int white, int g, ENode* parent, int step)
{
int i;
for (i = 0; i <= 8; i++)
{
Temp->status[i] = status[i];
}
Temp->White = white;
Temp->G = g;
Temp->H = CountH(status);
Temp->F = Temp->G + Temp->H;
Temp->Parent = parent;
Temp->step = step;
}
//判断子节点是否在OPEN或CLOSE中,并进行对应的操作
void ExistAndOperate(ENode newNode)
{
int i;
int inList; //定义表示新生成节点是否在OPEN表或CLOSE表中, 值为0 均不在,值>0 只在OPEN表,值<0 只在CLOSE表
if (newNode.G == 1) //如果是第一步的节点,直接加入OPEN中,返回
{
Insert(newNode);
return;
}
inList = Exist(newNode); //判断新节点是否在OPEN或CLOSE中
if (inList == 0) //如果均不在两个表中,将节点加入OPEN表中
{
Insert(newNode);
}
else if (inList > 0) //如果在OPEN中,说明从初始节点到该节点找到了两条路径,保留耗散值短的那条路径
{
if (OPEN[inList].F > newNode.F) //如果表内节点F值大于新节点F值,用新节点代替表内节点
{
for (i = inList; i < open - 1; i++)
OPEN[i] = OPEN[i + 1];
open--;
Insert(newNode);
}
}
}
//寻找最佳路径
ENode* Search()
{
int i, j, temp;
int status[9];
ENode* Temp = new ENode;
ENode* Node;
while (1) //一直循环知道找到解结束
{
Node = new ENode;
*Node = OPEN[0]; //从OPEN表中取出第一个元素(F值最小)进行扩展
//循环出口
if (Node->H == 0) //判断该节点是否是目标节点,若是,则不在位的将牌数为0,算法结束
{
break;
}
CLOSE[close] = *Node; //将扩展过的节点放入CLOSE
close++;
for (i = 0; i <= open - 1; i++) //将扩展的节点从OPEN中释放
{
OPEN[i] = OPEN[i + 1];
}
open--;
//对结点进行拓展,得到子节点
if ((Node->White) % 3 >= 1) //如果能左移,则进行左移创造新结点
{
for (i = 0; i <= 8; i++)
{
status[i] = Node->status[i];
}
temp = status[Node->White - 1];
status[Node->White - 1] = 8;
status[Node->White] = temp;
ENodeInit(Temp, status, Node->White - 1, (Node->G) + 1, Node, 1); //初始化新结点
ExistAndOperate(*Temp); //判断子节点是否在OPEN或CLOSE中,并进行对应的操作
}
if ((Node->White) % 3 <= 1) //如果能右移,则进行右移创造新结点
{
for (i = 0; i <= 8; i++)
{
status[i] = Node->status[i];
}
temp = status[Node->White + 1];
status[Node->White + 1] = 8;
status[Node->White] = temp;
ENodeInit(Temp, status, Node->White + 1, (Node->G) + 1, Node, 2); //初始化新结点
ExistAndOperate(*Temp); //判断子节点是否在OPEN或CLOSE中,并进行对应的操作
}
if (Node->White >= 3) //如果能上移,则进行上移创造新结点
{
for (i = 0; i <= 8; i++)
{
status[i] = Node->status[i];
}
temp = status[Node->White - 3];
status[Node->White - 3] = 8;
status[Node->White] = temp;
ENodeInit(Temp, status, Node->White - 3, (Node->G) + 1, Node, 3); //初始化新结点
ExistAndOperate(*Temp); //判断子节点是否在OPEN或CLOSE中,并进行对应的操作
}
if (Node->White <= 5) //如果能下移,则进行下移创造新结点
{
for (i = 0; i <= 8; i++)
{
status[i] = Node->status[i];
}
temp = status[Node->White + 3];
status[Node->White + 3] = 8;
status[Node->White] = temp;
ENodeInit(Temp, status, Node->White + 3, (Node->G) + 1, Node, 4); //初始化新结点
ExistAndOperate(*Temp); //判断子节点是否在OPEN或CLOSE中,并进行对应的操作
}
if (open == 0) //如果open=0, 证明算法失败, 没有解
{
return NULL;
}
}
return Node;
}
//展示具体步骤
void ShowStep(ENode* Node)
{
int STEP[MAXSTEPSIZE];
int STATUS[MAXSTEPSIZE][9];
int step = 0;
int i, j;
int totalStep = Node->G;
while (Node)
{
STEP[step] = Node->step;
for (i = 0; i <= 8; i++)
{
STATUS[step][i] = Node->status[i];
}
step++;
Node = Node->Parent;
}
printf("----------------------\n");
printf("所需最少步数:%d步\n", totalStep);
printf("----------------------\n");
for (i = step - 1; i >= 0; i--)
{
if (STEP[i] == 1)
printf("left");
else if (STEP[i] == 2)
printf("right");
else if (STEP[i] == 3)
printf("up");
else if (STEP[i] == 4)
printf("down");
else if (STEP[i] == 0)
printf("过程:\n");
printf(" ");
}
printf("\n节点变化:\n----------------------\n");
for (i = step - 1; i >= 0; i--)
{
for (j = 0; j <= 8; j++)
{
printf("%d",STATUS[i][j]);
if (j == 2 || j == 5 || j == 8)
printf("\n");
else
printf(" ");
}
printf("----------------------\n");
}
}
int main()
{
int map[3][3];
int fstatus[9] = { 0 };
srand((unsigned int)time(0));//随机函数种子
initmap(map);
while (!(isSolvable(map)))
{
initmap(map);
} //重复打乱直到可以还原
int i,j,index=0;
//map数组转化为fstatus
for (i = 0; i < 3; i++)
{
for (j = 0; j < 3; j++)
{
fstatus[index] = map[i][j];
index++;
}
}
ENode* FNode=new ENode;
ENode* EndNode;
for (i = 0; i <= 8; i++) //判断0节点位置
{
if (fstatus[i] == 8)
break;
}
ENodeInit(FNode, fstatus, i, 0, NULL, 0); //初始节点
OPEN[open] = *FNode; //将初始节点放入OPEN中
open++;
EndNode = Search(); //寻找最佳路径
if (!EndNode)
printf("无解");
else
{
ShowStep(EndNode); //展示步骤
}
return 0;
}
另一种解决方法
可以发现寻找最优解就是在一张图中寻找两个节点间的最优路径,不同的是这张图是在寻找解的过程中不断生成的。
实际上,我们可以穷举出所有节点,对于三阶九宫格,共有个节点,而任意两个节点间,只要能通过白块的一次移动相互转换,就把两个节点相连,并记录两个节点转换的方式,最后得到一张无向图。
如果将最终的节点状态设置为0,那么问题就转化为在这张无向图上寻找任意节点到0节点的最短路径,使用广度优先搜索便能够找到最优解。
而这张无向图是不会变的,可以先编写一个程序算出这张无向图,并以适当的形式保存下来,然后在我们的游戏程序中,就读入无向图,然后使用广度优先搜索寻找最优解。
这对于更高阶的拼图游戏,可能是一种解决方案,因为在更高阶的拼图中,A*算法求解需要的时间很长,可能是我程序写得有问题,我用A*算法很少在10分钟内跑出结果。
进一步的,可以想到,任意节点到0节点的最短路径也是固定的。为了更快速的得到结果,我们甚至可以先计算出每个节点到0节点的最优路径,并保存在自己电脑上,然后需要某个节点到0节点的最优路径时,只需要查询到初始节点为该节点的那条路径就可以了。不过由于节点数量众多,这样到底能否更快地得出最优路径,还需要等待各位有想法的人验证。