该文将通过对话的形式,介绍一个童年的回忆 —— 推箱子 游戏。
这个故事发生在 ❤️学姐教我十道题搞定C语言❤️ 后的三个月,我开始和大部分大学生一样沉迷游戏,这时候学姐为了助我脱坑,竟然要求教我做游戏,也就是因为这么一教,让我入了游戏坑,一做就是十四年!那是我逝去的青春!
在此也劝诫现在在校打算出来找工作的大学生,千万要选一个你认为能够坚持一辈子做下去的行业,不然中途想转行真的很难!除非你能够接受重新学习和平薪或者降薪切换!
1)一些基本的输入输出、循环迭代、递归语法;
2)一些 STL 队列 queue 的接口;
3)一些控制台的显示接口;
□(空心方块)代表空地
■(实心方块)代表障碍物
●(实心圆)代表箱子
◎(同心圆)代表目标位置
♂(男性符号)代表推箱子的人
enum BlockType {
BT_EMPTY = 0, // 空地
BT_WALL = 1, // 障碍物、墙
BT_BOX = 2, // 箱子
BT_TARGET = 3, // 目标位置
BT_MAN = 4, // 推箱子的人
BT_MAX
};
// 这些都是宽字符,占用2个字节,加上字符串末尾 '\0',总共3个字节
const char BlockSign[BT_MAX][3] = {
"□", "■", "●", "◎", "♂" };
BlockType convertCharToBlockType(char chigh, char clow) {
BlockType tp = BlockType::BT_EMPTY;
for (int i = BlockType::BT_EMPTY; i < BlockType::BT_MAX; ++i) {
if (BlockSign[i][0] == chigh && BlockSign[i][1] == clow) {
tp = (BlockType)i;
break;
}
}
return tp;
}
假设一个 R × C R \times C R×C 的地图上,我们令 “小人” 的位置为 ( m x , m y ) (m_x,m_y) (mx,my), n n n 个箱子的位置分别为 ( b 1 x , b 1 y ) , ( b 2 x , b 2 y ) , . . . , ( b n x , b n y ) (b1_x,b1_y),(b2_x,b2_y),..., (bn_x,bn_y) (b1x,b1y),(b2x,b2y),...,(bnx,bny) ,并且所有 x x x 坐标都是在 [ 0 , R ) [0, R) [0,R) 范围内,所有 y y y 坐标都是在 [ 0 , C ) [0, C) [0,C) 范围内;
为了简化问题, 我们将位置再进行一次转换,把二维转换成一维,即:
( m x , m y ) = m p o s = m x × C + m y ( b 1 x , b 1 y ) = b 1 p o s = b 1 x × C + b 1 y . . . ( b n x , b n y ) = b n p o s = b n x × C + b n y (m_x,m_y) = m_{pos} = m_x \times C + m_y \\ (b1_x,b1_y) = b1_{pos} = b1_x \times C + b1_y \\ ...\\ (bn_x,bn_y) = bn_{pos} = bn_x \times C + bn_y (mx,my)=mpos=mx×C+my(b1x,b1y)=b1pos=b1x×C+b1y...(bnx,bny)=bnpos=bnx×C+bny
将这 n + 1 n+1 n+1 个对象的位置看成是 K K K 进制的每一位,就可以编码成一个数字 x x x 了(其中 K = R × C K = R \times C K=R×C );
x = m p o s × K 0 + b 1 p o s × K 1 + b 2 p o s × K 2 + . . . + b n p o s × K n x = m_{pos} \times K^0 + b1_{pos} \times K^1 + b2_{pos} \times K^2 + ... + bn_{pos} \times K^n x=mpos×K0+b1pos×K1+b2pos×K2+...+bnpos×Kn
这就将原本 2 ∗ ( n + 1 ) 2*(n+1) 2∗(n+1) 维的向量降成 1 维的了。
1)从小人的初始位置进行一次深度优先搜索(认为除了墙以外其它点都可达),标记所有遍历到的点;
2)对以上所有深搜遍历到的点从 1 开始进行编号,所有这些点编号不重复即可。如图三-2-2所示:
图三-2-2
3)小人和箱子到达的位置直接用编码后的数字表示即可,这样一来,在压缩成 K 进制数的时候,K的值从原来的 R x C = 64 变成了 24,大大缩小了状态空间。状态编码的时候,为了让解码的时候能够准确解出有多少个箱子,编码位从1开始(如果从0开始,那么一旦有个箱子在0的位置会产生二义性)。
4)箱子的编码还是遵从最小表示法,按照递增顺序排完序再压缩到一维。
任何情况下,小人都是选择往四个方向走一格,对于每个方向,都有3种情况:
1)前面没路(墙或者越界),这种情况无法扩展状态;
2)前面没有箱子,直接往前走,扩展状态(塞入队列尾部);
3)前面有个箱子,又分两种情况:
3.1)箱子前面无法放置,这种情况无法扩展状态;
3.2)箱子前面可以放置,人和箱子同时往这个方向前进一格,扩展状态(塞入队列尾部);
const int MAXN = 10;
class PushBoxGame {
public:
// 公有接口
private:
// 私有函数
private:
// 关卡相关
int row_, col_; // 游戏关卡行和列
BlockType blocks_[MAXN][MAXN]; // 游戏关卡地图
// 深搜相关
int id_[MAXN][MAXN]; // 关卡格子编号,idCount_为上文提到的 K
int idrow_[MAXN*MAXN]; // id 行反查
int idcol_[MAXN*MAXN]; // id 列反查
// 广搜相关
Path path_; // 路径生成器
Hash hash_; // 标记状态的hash表
int finalState_; // 搜索的最终状态
};
1)地图大小不超过 10X10
2)箱子最多个数 6
3)目标位置必须 和 箱子数匹配
4)必须严格有一个"小人"
bool PushBoxGame::isBlockValid() {
// 1 地图大小最大 MAXN X MAXN
if (row_ > MAXN || col_ > MAXN) {
return false;
}
int blockCnt[BlockType::BT_MAX];
memset(blockCnt, 0, sizeof(blockCnt));
for (int i = 0; i < row_; ++i) {
for (int j = 0; j < col_; ++j) {
++blockCnt[blocks_[i][j]];
}
}
// 2 箱子最多个数 MAXBOX
if (blockCnt[BlockType::BT_BOX] > MAXBOX) {
return false;
}
// 3 目标位置必须 和 箱子数匹配
if (blockCnt[BlockType::BT_TARGET] != blockCnt[BlockType::BT_BOX]) {
return false;
}
// 4 必须严格有一个 '小人'
if (blockCnt[BlockType::BT_MAN] != 1) {
return false;
}
return true;
}
1)初始化所有格子的标记为 VT_UNVISITED(-1)
2)从 “小人” 位置出发,遍历所有与之相邻的连通块,将遍历到的块标记位 VT_VISITED(0);
3)按照从左往右,从上往下的顺序对所有已访问格子进行不重复编号;
enum VisitType {
VT_UNVISITED = -1,
VT_VISITED = 0,
};
memset(id_, VisitType::VT_UNVISITED, sizeof(id_));
void PushBoxGame::genId_floodfill() {
// 1.初始化所有格子为未访问
memset(id_, VisitType::VT_UNVISITED, sizeof(id_));
// 2.找到 BT_MAN 的格子进行深搜,标记访问到的格子
for (int i = 0; i < row_; ++i) {
for (int j = 0; j < col_; ++j) {
if (blocks_[i][j] == BT_MAN) {
genId_dfs(i, j);
}
}
}
}
1)障碍检测;
2)重复访问检测;
3)标记当前格子已访问;
4)递归处理相邻四个格子
const int dir[4][2] = {
{
0, 1 }, {
0, -1 }, {
1, 0 }, {
-1, 0 }
};
bool PushBoxGame::isInBound(int r, int c) {
// 越界检测
return r >= 0 && r < row_ && c >= 0 && c < col_;
}
bool PushBoxGame::isInWall(int r, int c) {
// 墙体障碍检测
return blocks_[r][c] == BT_WALL;
}
// 综合障碍检测
bool PushBoxGame::isObstacle(int r, int c) {
return !isInBound(r, c) || isInWall(r, c);
}
void PushBoxGame::genId_dfs(int r, int c) {
// 1. 障碍检测
if (isObstacle(r, c)) {
return;
}
// 2. 重复访问检测
if (id_[r][c] == VisitType::VT_VISITED) {
return;
}
// 3. 标记当前格子已访问
id_[r][c] = VisitType::VT_VISITED;
// 4. 递归处理相邻四个格子
for (int i = 0; i < 4; ++i) {
genId_dfs(r + dir[i][0], c + dir[i][1]);
}
}
void PushBoxGame::genId_genTerr() {
printf("生成地形数据...\n");
// 1. 按照从左往右,从上往下的顺序标记所有已访问格子
int idCount = 1;
for (int i = 0; i < row_; ++i) {
for (int j = 0; j < col_; ++j) {
if (id_[i][j] == VisitType::VT_VISITED) {
// 添加 id 正向映射
id_[i][j] = idCount++;
// 添加 id 反向映射
idrow_[id_[i][j]] = i;
idcol_[id_[i][j]] = j;
}
}
}
PushBoxState::setBase(idCount);
}
void PushBoxGame::genId() {
genId_floodfill();
genId_genTerr();
}
class Hash {
public:
Hash();
virtual ~Hash();
private:
bool *hashkey_; // 状态hash的key
StateType *hashval_; // 状态hash的val
public:
// 销毁调用
void finalize();
// 初始化调用
void initialize();
// 获取给定值的哈希值
int getKey(StateType val);
// 查询是否有这个值在哈希表中
bool hasKey(StateType val);
// 获取给定哈希值的原值
StateType getValue(int key);
};
void Hash::finalize() {
if (hashkey_) {
delete[] hashkey_;
hashkey_ = NULL;
}
if (hashval_) {
delete[] hashval_;
hashval_ = NULL;
}
}
void Hash::initialize() {
// 1. 释放空间避免内存泄漏
// 2. 初始化哈希的key和val
if (!hashkey_) {
hashkey_ = new bool[MAXH + 1];
}
if (!hashval_) {
hashval_ = new StateType[MAXH + 1];
}
memset(hashkey_, false, (MAXH + 1) * sizeof(bool));
}
int Hash::getKey(StateType val) {
// 1. 采用 位与 代替 取模,位运算加速
int key = (val & MAXH);
while (1) {
if (!hashkey_[key]) {
// 2. 如果对应的key没有出现过,则代表没有冲突过;则key的槽位留给val;
hashkey_[key] = true;
hashval_[key] = val;
return key;
}
else {
if (hashval_[key] == val) {
// 3. 如果key 的槽位正好和val匹配,则说明找到了,返回 key;
return key;
}
// 4. 没有找到合适的 key, 进行二次寻址
key = (key + 1) & MAXH;
}
}
}
a)方便倍增进行 rehash;
b)位运算的运算效率高于取模,所以可以用 位与 2 n − 1 2^n-1 2n−1 来代替对 2 n 2^n 2n 取模;
2)!hashkey_[key]
代表对应的key没有出现过,即没有和其他值产生冲突过,则key的槽位留给 val;
3)hashval_[key] == val
代表这个key的槽位之前和val的值是一一映射的,则直接范围 key 的值即可;
4)key = (key + 1) & MAXH;
进行二次寻址,继续寻找合适的 key 槽位;
然后再提供一个反查接口 getValue,即根据 key 查询 value,如下:
StateType Hash::getValue(int key) {
if (key < MAXH && hashkey_[key]) {
return hashval_[key];
}
return -1;
}
StateType Serialize(int man, int boxcnt, int box[MAXBOX]);
void DeSerialize(StateType state);
Serialize
根据传入的 人和箱子位置,生成一个整数,暂且称之为序列化;DeSerialize
根据传入的整数,反算出 人和箱子 位置,暂且称之为反序列化;// 注意:状态编码的时候,为了让解码的时候能够准确解出有多少个箱子,编码位 为不设置 0
class PushBoxState {
public:
static void setBase(int b);
static int getBase();
PushBoxState();
virtual ~PushBoxState();
// 根据传入的 人和箱子位置,生成一个整数
StateType Serialize(int man, int boxcnt, int box[MAXBOX]);
// 根据传入的整数,反推出 人和箱子位置
void DeSerialize(StateType state);
// 对私有成员访问的封装
StateType getBoxState();
StateType getState();
void setManCode(int val);
int getManCode();
void setBoxCode(int idx, int val);
int getBoxCode(int idx);
int getBoxCount();
// 获取是否有一个箱子在id上
// 有的话,返回箱子下标,否则返回 -1
int getMatchBoxIndex(int id);
private:
void calcState(bool bReCalcBox);
void calcManCode();
void calcBoxCode();
private:
int man_;
int boxcnt_;
int box_[MAXBOX];
StateType boxstate_;
StateType state_;
static int base_;
};
bool PushBoxGame::bfs() {
bfs_initialize();
bfs_pushInitState();
while(!queue.empty()) {
bfs_popFrontState();
bfs_checkFinalState();
bfs_extendState();
}
}
bool PushBoxGame::bfs() {
queue <int> Q;
PushBoxState pbs;
bfs_initialize();
// 提前计算出终止状态
StateType finalBoxState = getFinalBoxState();
// 将初始状态压入队列
int startState = hash_.getKey(getInitState());
Q.push(startState);
while (!Q.empty()) {
int nowState = Q.front();
Q.pop();
// 将编码后的数据 反序列化 到 pbs
pbs.DeSerialize(hash_.getValue(nowState));
// 找到解,将最终状态持久化
if (pbs.getBoxState() == finalBoxState) {
finalState_ = nowState;
return true;
}
// 人往四个方向走一格,对于每个方向,都有3种情况:
// 1. 前面没路,这种情况无法扩展状态;
// 2. 前面没有箱子,直接往前走,扩展状态;
// 3. 前面有个箱子,又分两种情况:
// 3.1 箱子前面无法放置,这种情况无法扩展状态;
// 3.2 箱子前面可以放置,人和箱子同时往这个方向前进一格,扩展状态;
int man = pbs.getManCode();
for (int i = 0; i < 4; ++i) {
int manr = idrow_[man] + dir[i][0];
int manc = idcol_[man] + dir[i][1];
if (isObstacle(manr, manc)) {
// 情况1
continue;
}
int nextman = id_[manr][manc];
// 模拟人走到了这个位置
pbs.setManCode(nextman);
int boxIndex = pbs.getMatchBoxIndex(nextman);
if (boxIndex == -1) {
// 情况2
bfs_checkAndExtendState(Q, nowState, pbs);
}
else {
// 情况3 箱子必须往前推进一格
int boxr = idrow_[nextman] + dir[i][0];
int boxc = idcol_[nextman] + dir[i][1];
if (isObstacle(boxr, boxc) || pbs.getMatchBoxIndex(id_[boxr][boxc]) != -1) {
// 情况3.1
continue;
}
// 情况3.2
// 模拟箱子往前走了一格
pbs.setBoxCode(boxIndex, id_[boxr][boxc]);
bfs_checkAndExtendState(Q, nowState, pbs);
// 回退箱子
pbs.setBoxCode(boxIndex, nextman);
}
}
}
return false;
}