问题 V: 推箱子游戏-深度优先搜索版本
题目描述
推箱子是一款经典游戏。这里我们玩的是一个简单版本,就是在一个N*M的地图上,有1个玩家、1个箱子、1个目的地以及若干障碍,其余是空地。玩家可以往上下左右4个方向移动,但是不能移动出地图或者移动到障碍里去。如果往这个方向移动推到了箱子,箱子也会按这个方向移动一格,当然,箱子也不能被推出地图或推到障碍里。当箱子被推到目的地以后,游戏目标达成。现在告诉你游戏开始是初始的地图布局,要求用深度优先搜索找到游戏的解(注意这里不保证步数最少)。
玩家每到一个格子,就按上(U),右®,下(D),左(L)顺时针方向尝试,每一个方向都都在前一个方向失败时才可能尝试。如下图,如果s6为终态,则游戏解为UU; 如果s21为终态,则玩家要尝试UU,UR,UD,UL,RU,RR,RD,RL,…,LD, 才能确定LL是游戏的解。
状态由玩家位置和箱子位置构成,算法结构大体如下:
DFS(state s){
for(i = 0; i < 4; i++)
DFS(trans(s, i)); //tans表示状态s往方向i走形成的新状态
}
注意得到解后要立即返回。
输入
第一行输入两个数字N,M表示地图的大小。其中0<N,M<=12。
接下来有N行,每行包含M个字符表示游戏地图。其中 . 表示空地、X表示玩家、*表示箱子、#表示障碍、@表示目的地。
输出
有解时,输出玩家走的每一步。当无论如何达成不了的时候,输出-1。
样例输入 Copy
6 6
...#..
......
#*##..
..##.#
..X...
.@#...
样例输出 Copy
RRUUULLULDDUURDRRURDDLDDRDLLULLUUDDRRRUUULLULLDRRRLULDDDDRRRUURUULDDDUULLULDDDDRRDRRULLLRRUUULLULDDDD
提示
为了帮助大家理解,下面再给出两组数据
3 3
X..
#*.
@..
对应的DFS
RDURDDL
3 3
X.@
.*.
...
对应的DFS
RRDDLURDLLUUR
实现过程
使用C语言编写使用深度优先搜索(DFS)实现推箱子游戏的代码是一项综合性的编程任务,它涉及到递归、搜索算法和游戏逻辑。以下是对该项目的整体分析和目的的讨论:
整体分析:
-
问题理解:
推箱子游戏是一个典型的搜索问题,目标是在有限的空间内通过推动箱子到达指定位置。 -
算法选择:
深度优先搜索适合于解决这类问题,因为它能够深入探索每一种可能的移动,直到找到解决方案或确定无解。 -
数据结构:
- 使用二维数组表示游戏地图,包括玩家、箱子、空地和障碍物。
- 使用结构体或类来表示游戏状态,可能包括玩家位置、箱子位置和目标状态。
-
递归实现:
DFS通过递归调用来实现,每次递归尝试一种移动,并回溯以探索其他可能性。 -
搜索策略:
- 定义搜索的深度,即玩家可以移动的最大步数。
- 确保在搜索过程中不重复访问同一状态。
-
状态管理:
使用栈数据结构来存储递归调用的状态,包括当前位置和移动历史。 -
边界和规则检查:
在每次移动后,检查是否超出边界、是否移动到障碍物上或是否已经达到目标状态。 -
回溯机制:
当当前路径无法到达目标时,使用回溯返回到上一个决策点,并尝试其他移动。 -
代码实现:
- 实现递归函数来执行搜索。
- 实现辅助函数来处理移动、状态复制和目标检测。
-
测试和验证:
编写测试用例来验证算法的正确性和有效性。
目的:
-
算法学习:
通过实现DFS,深入理解其工作原理和适用场景。 -
问题解决能力:
提升解决复杂问题的能力,学习如何将问题分解为可管理的子问题。 -
编程技能:
增强C语言编程技能,特别是在递归、指针和数据结构方面的应用。 -
游戏AI开发:
为推箱子游戏开发AI,提供自动化解决方案,增加游戏的互动性和趣味性。 -
搜索算法应用:
探索搜索算法在游戏开发和其他领域的应用,如路径规划、决策制定等。 -
逻辑思维和创新:
锻炼逻辑思维,鼓励创新性地思考问题和解决方案。
这段C++代码实现了一个基于深度优先搜索(DFS)算法的迷宫问题求解器,用于解决类似于推箱子这样的游戏。
下面是对代码的详细解析:
-
头文件和命名空间:
- 包含
<iostream>
、<stdlib.h>
、<string.h>
和<stdio.h>
头文件。 - 使用
using namespace std;
来避免在标准库类型和函数前加std::
。
- 包含
-
全局变量:
zmap
:二维字符数组,表示迷宫地图。jmap
:四维整型数组,用于标记是否已经访问过某个状态。n1, m1
:表示终点的位置。n2, m2
:表示起点(人)的位置。n3, m3
:表示箱子的位置。n, m
:表示地图的尺寸。people
:表示人可以移动的四个方向。road
:记录人走的路径。k
:记录路径长度。vroad
:与people
方向对应,记录移动方向的字符。liu
:记录到达终点后的返回值。
-
边界检查函数
OutMap
:- 检查给定的位置是否越界或是否为墙壁(
'#'
),如果是,则返回true
。
- 检查给定的位置是否越界或是否为墙壁(
-
深度优先搜索算法
venture
:- 递归函数,用于探索从当前状态到终点的所有可能路径。
- 如果到达终点,返回
1
并记录路径;如果无法到达终点,返回0
。
-
主函数
main
:- 读取地图尺寸
n
和m
。 - 读取地图布局,同时初始化人、箱子和终点的位置。
- 调用
venture
函数进行搜索。 - 如果
k
为0
,表示无法到达终点,输出-1
;否则,逆序输出路径。
- 读取地图尺寸
-
程序结束:
- 输出搜索结果后,程序结束。
代码逻辑分析:
- 这段代码使用深度优先搜索算法来寻找从起点到终点的路径,同时需要推动箱子到达指定位置。
- 使用一个四维数组
jmap
来记录访问过的状态,避免重复访问。 - 使用一个字符串
road
来记录从起点到终点的路径。
潜在问题:
- 使用
scanf
读取字符串时,没有指定字符串的最大长度,可能导致缓冲区溢出。
改进建议:
- 使用
std::cin
和std::getline
替代scanf
来读取地图布局,以提高代码的安全性和可读性。 - 考虑使用
std::vector
或std::array
替代原始数组,以提高代码的灵活性和健壮性。 - 可以添加对输入数据有效性的检查,确保读取的是有效的地图布局。
DFS算法讲解
**深度优先搜索(Depth-First Search, DFS)**是一种用于遍历或搜索树或图的算法。它从一个节点开始,沿着树的深度遍历节点,尽可能深地搜索树的分支。当节点v的子节点都已被访问过,搜索将回溯到节点v的父节点,继续搜索其余的子节点。
核心概念:
- 递归:DFS通常使用递归实现,每次递归处理一个节点的所有邻接节点。
- 栈:在非递归实现中,使用栈来保存待访问的节点。
- 回溯:当当前路径无法到达目标时,算法会回溯到上一个决策点。
算法步骤:
- 选择起始节点:从图中的任意节点开始。
- 访问节点:标记起始节点为已访问。
- 探索邻接节点:对起始节点的未访问邻接节点进行探索:
a. 将邻接节点入栈。
b. 访问该邻接节点。
c. 重复步骤3,直到该邻接节点的所有邻接节点都被访问。 - 回溯:当当前节点的所有邻接节点都被访问后,从栈中弹出该节点,回到上一个节点继续探索。
- 终止条件:当所有节点都被访问或找到目标节点时,搜索结束。
应用场景:
- 路径搜索:在迷宫或图中寻找路径。
- 拓扑排序:对有向无环图的顶点进行排序。
- 解决数独:自动填充数独游戏。
- 游戏AI:用于游戏如国际象棋、围棋等的算法决策。
代码实现:
DFS可以通过递归或非递归(使用栈)的方式实现。以下是使用递归的简单C语言实现示例:
#include <stdio.h>
#include <stdbool.h>
#define MAX_VERTICES 1000 // 假设最大顶点数
// 假设图使用邻接矩阵表示
int graph[MAX_VERTICES][MAX_VERTICES];
bool visited[MAX_VERTICES]; // 跟踪节点是否被访问
// 用于DFS的辅助函数
void dfs(int v, int current_depth) {
visited[v] = true; // 标记当前节点为已访问
printf("Visited %d at depth %d\n", v, current_depth);
// 遍历所有未访问的邻接节点
for (int i = 0; i < MAX_VERTICES; ++i) {
if (graph[v][i] && !visited[i]) {
dfs(i, current_depth + 1);
}
}
}
int main() {
// 初始化图和访问数组
// ...
// 调用DFS从节点0开始
dfs(0, 0);
return 0;
}
注意事项:
- 确保在递归实现中对每个节点只访问一次,避免无限递归。
- 在非递归实现中,正确管理栈的使用,避免内存泄漏。
- 对于大型图,DFS可能需要优化以减少内存消耗和提高效率。
DFS是一种强大的搜索方法,适用于从图中寻找路径或解决需要递归和回溯的问题。通过实际应用,可以加深对DFS原理的理解,并提高解决复杂问题的能力。
部分实现
判断要走的位置是否符合规定
int OutMap(int a, int b)
{
if (a >= 0 && a < n && b >= 0 && b < m && zmap[a][b] != '#')
return 0;
else
return 1;
}
深度优先算法
int venture(int p1, int p2, int x1, int x2)
{
//如果箱子位置与终点位置重合则探索结束进行回溯
if (x1 == n1 && x2 == m1)
{
return 1;
}
//分别判断此时位置的四个方向是否可走
for (int i = 0; i < 4; i++)
{
//若沿此方向走合法
if (!OutMap(p1 + people[i][0], p2 + people[i][1]))
{
//若沿此方向走到达位置与箱子位置重合
if (p1 + people[i][0] == x1 && p2 + people[i][1] == x2)
{
//若往此方向推进箱子合法
if (!OutMap(x1 + people[i][0], x2 + people[i][1]))
{
//若此方向(箱子位置改变)到达位置没有走过
if (!jmap[p1 + people[i][0]][p2 + people[i][1]][x1 + people[i][0]][x2 + people[i][1]])
{
//标记走过
jmap[p1 + people[i][0]][p2 + people[i][1]][x1 + people[i][0]][x2 + people[i][1]] = 1;
//如果到达终点得到返回值1则将这一操作存进字符串并回溯
liu = venture(p1 + people[i][0], p2 + people[i][1], x1 + people[i][0], x2 + people[i][1]);
if (liu == 1)
{
road[k++] = vroad[i];
liu = 0;
return 1;
}
}
}
}
//若沿此方向走到达位置与箱子位置没有重合
else
{
//若此方向(箱子位置没有改变)到达位置没有走过
if (!jmap[p1 + people[i][0]][p2 + people[i][1]][x1][x2])
{
//标记走过
jmap[p1 + people[i][0]][p2 + people[i][1]][x1][x2] = 1;
liu = venture(p1 + people[i][0], p2 + people[i][1], x1, x2);
//如果到达终点得到返回值1则将这一操作存进字符串并回溯
if (liu == 1)
{
road[k++] = vroad[i];
liu = 0;
return 1;
}
}
}
}
}
//没有到达终点
return 0;
}
AC代码
#include <iostream>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
using namespace std;
char zmap[20][20]; //地图
int jmap[20][20][20][20] = { 0 }; //标记是否走过
int n1, m1; //终点位置
int n2, m2; //起点位置
int n3, m3; //箱子位置
int n, m; //地图大小
int people[4][2] = //人走的位置变化
{
{-1,0},
{0,1},
{1,0},
{0,-1},
};
char road[100007]; //标记人走的路径
int k = 0;
char vroad[4] = { 'U','R','D','L' }; //标记行走方向
int liu = 0; //记录到达终点以后的返回值
//判断要走的位置是否符合规定
int OutMap(int a, int b)
{
if (a >= 0 && a < n && b >= 0 && b < m && zmap[a][b] != '#')
return 0;
else
return 1;
}
//深度优先探索
int venture(int p1, int p2, int x1, int x2)
{
//如果箱子位置与终点位置重合则探索结束进行回溯
if (x1 == n1 && x2 == m1)
{
return 1;
}
//分别判断此时位置的四个方向是否可走
for (int i = 0; i < 4; i++)
{
//若沿此方向走合法
if (!OutMap(p1 + people[i][0], p2 + people[i][1]))
{
//若沿此方向走到达位置与箱子位置重合
if (p1 + people[i][0] == x1 && p2 + people[i][1] == x2)
{
//若往此方向推进箱子合法
if (!OutMap(x1 + people[i][0], x2 + people[i][1]))
{
//若此方向(箱子位置改变)到达位置没有走过
if (!jmap[p1 + people[i][0]][p2 + people[i][1]][x1 + people[i][0]][x2 + people[i][1]])
{
//标记走过
jmap[p1 + people[i][0]][p2 + people[i][1]][x1 + people[i][0]][x2 + people[i][1]] = 1;
//如果到达终点得到返回值1则将这一操作存进字符串并回溯
liu = venture(p1 + people[i][0], p2 + people[i][1], x1 + people[i][0], x2 + people[i][1]);
if (liu == 1)
{
road[k++] = vroad[i];
liu = 0;
return 1;
}
}
}
}
//若沿此方向走到达位置与箱子位置没有重合
else
{
//若此方向(箱子位置没有改变)到达位置没有走过
if (!jmap[p1 + people[i][0]][p2 + people[i][1]][x1][x2])
{
//标记走过
jmap[p1 + people[i][0]][p2 + people[i][1]][x1][x2] = 1;
liu = venture(p1 + people[i][0], p2 + people[i][1], x1, x2);
//如果到达终点得到返回值1则将这一操作存进字符串并回溯
if (liu == 1)
{
road[k++] = vroad[i];
liu = 0;
return 1;
}
}
}
}
}
//没有到达终点
return 0;
}
//主函数
int main()
{
//输入地图大小
cin >> n >> m;
//输入地图元素并标记起点、终点以及箱子的位置
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
{
//地图元素
cin >> zmap[i][j];
//终点
if (zmap[i][j] == '@')
{
n1 = i;
m1 = j;
}
//起点
if (zmap[i][j] == 'X')
{
n2 = i;
m2 = j;
}
//箱子位置
if (zmap[i][j] == '*')
{
n3 = i;
m3 = j;
}
}
int p1 = n2, p2 = m2, x1 = n3, x2 = m3;
venture(p1, p2, x1, x2);
road[k] = '\0';
//无法到达终点
if (k == 0)
cout << "-1";
//到达终点输出操作
else
{
for (int i = k - 1; i >= 0; i--)
printf("%c", road[i]);
}
}