主頁 >  其他 > 從演算法角度剖析兒時的回憶 —— 推箱子

從演算法角度剖析兒時的回憶 —— 推箱子

2020-12-25 12:30:11 其他

文章目錄

  • 一、前言
  • 二、預備知識
    • 1、基礎 c/c++ 語法
    • 2、數學基礎排列組合
    • 3、深度、廣度優先搜索
    • 4、哈希表
  • 三、演算法分析
    • 1、資料表示
    • 2、演算法設計
      • 1)演算法方向確定
      • 2)狀態表示
      • 3)狀態降維
      • 4)狀態壓縮
      • 5)搜索
        • 1. 初始狀態生成
        • 2. 狀態擴展
        • 3. 結束狀態判定
  • 四、編碼實作
    • 1、類的定義
    • 2、輸入合法性判定
    • 3、深搜實作格子編號
      • 1)標記格子未訪問
      • 2)深搜訪問所有連通塊
      • 3)對訪問到的塊進行重編號
      • 4)介面封裝
    • 4、實作哈希函式
      • 1)設計一個哈希類
      • 2)初始化哈希陣列
      • 3)狀態哈希映射實作
      • 4)狀態編碼實作
    • 5、廣搜模擬推箱子程序
    • 6、路徑回溯
    • 7、效果渲染
  • 五、寫在最后

一、前言

  • 看到這張圖,相信大部分人都不陌生,沒錯,這就是我們那個年代非常流行的益智游戲 —— 推箱子,
    圖一-1
  • 今天,我們來研究一下,如何通過計算機的角度,讓這個小人把所有箱子歸位,
  • 先來看下我們最后想要實作的效果:
    在這里插入圖片描述
    圖一-2

二、預備知識

  • 需要用到的預備知識有:

1、基礎 c/c++ 語法

  • 本文涉及的代碼語法均為 c/c++,包含但不限于:

1)一些基本的輸入輸出、回圈迭代、遞回語法;
2)一些 STL 佇列 queue 的介面;
3)一些控制臺的顯示介面;

2、數學基礎排列組合

  • 主要是涉及到狀態的計算,需要用到組合數;只需要了解 n n n 個元素中取 m m m 個元素的方案數為:
    C n m = n ! m ! ( n ? m ) ! C_n^m = \frac {n!}{m!(n-m)!} Cnm?=m!(n?m)!n!?

3、深度、廣度優先搜索

  • 深度、廣度優先搜索作為最主要的圖論遍歷演算法的,有著廣泛的應用,游戲中的尋路演算法就是基于廣搜的 A*;
  • 可以大致瀏覽一下這篇文章,有一個初步了解:夜深人靜寫演算法(一)搜索入門

4、哈希表

  • 哈希表主要用于廣搜的時候進行狀態標記,避免搜索重復狀態,將原本 多維 的狀態壓縮到 一維 后進行哈希;
  • 我之前也有介紹過哈希表,具體可以參見這篇文章:Redis底層詳解(一) 哈希表和字典

三、演算法分析

1、資料表示

  • 通過 ASCII 符號來表示不同的游戲塊:

□(空心方塊)代表空地
■(實心方塊)代表障礙物
●(實心圓)代表箱子
◎(同心圓)代表目標位置
♂(男性符號)代表推箱子的人

  • 將 圖一-1 中的游戲關卡表示成 ASCII 如圖三-1-1所示:
    圖三-1-1
  • 當然,在實際編碼中,每個格子是用列舉來表示的,代碼如下:
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] = {
    "□", "■", "●", "◎", "♂"
};
  • 這里的數字代表了游戲中實際每個方塊的含義,屬于資料層,而顯示層進行渲染的時候其實也是通過不同的資料來顯示成不同的畫面,可以是 ASCII 字符,也可以是圖片,甚至還可以是 3D 的,

    圖三-1-2

  • 為了將輸入資料轉換成我們可以處理的資料,需要提供轉換介面如下,對于給定的輸入遍歷尋找對應匹配的列舉:

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;
}

2、演算法設計

1)演算法方向確定

  • 首先,一定是用搜索演算法;
  • 其次,盡量肯定是要求最少步數把所有箱子推到目標位置,也就是求最短路問題,那么就是一個很明顯的 廣度優先搜索 問題(當然,也可以用 迭代加深 逐步擴大解空間來求解,今天就只介紹廣搜吧),

2)狀態表示

  • 演算法確定后,就需要確定如何進行狀態表示了;
  • 演算法的焦點一定在這個 “小人” 身上,但是光用 “小人” 的位置來表示狀態肯定是不夠的;如圖三-2-1所示,兩個地圖關卡的小人的位置是相同的,但是不能作為同一種情況來考慮,因為箱子的位置不同,所以最終狀態表示也不同,
    在這里插入圖片描述
    圖三-2-1
  • 于是,我們發現,狀態和所有能夠動的物件(人、箱子)有關系,那么可以把所有人和箱子的位置聯合起來作為狀態表示的依據,即: ( M a n p o s , B o x [ 1 ] p o s , B o x [ 2 ] p o s , . . . , B o x [ n ] p o s ) (Man_{pos}, Box[1]{pos},Box[2]{pos}, ..., Box[n]{pos}) (Manpos?,Box[1]pos,Box[2]pos,...,Box[n]pos)
  • 這是一個 2 ? n + 2 2*n+2 2?n+2 的向量,不利于哈希映射,所以我們需要把這些向量壓縮到一個數字上,

3)狀態降維

  • 假設一個 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 * C + m_y \\ (b1_x,b1_y) = b1_{pos} = b1_x * C + b1_y \\ ...\\ (bn_x,bn_y) = bn_{pos} = bn_x * 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 進制的每一位,就可以編碼成一個數字 x x x 了(其中 K = R ? C K = R*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} * K^0 + b1_{pos} * K^1 + b2_{pos} * K^2 + ... + bn_{pos} * K^n x=mpos??K0+b1pos??K1+b2pos??K2+...+bnpos??Kn

  • 這就將原本 2 ? ( n + 1 ) 2*(n+1) 2?(n+1) 維的向量降成 1 維的了,

4)狀態壓縮

  • 上面提到的位置中,其實有很多位置是無用位置,也就會有很多無用狀態帶出來,比如:墻的位置、不可達的空地等等;
  • n n n 個箱子是無差別的,作為 K K K 進制的不同權值的位似乎不太合適,所以這里涉及到一個 最小表示法 的問題,可以將所有箱子的數字進行從小到大排序,再進行狀態降維,如下的三個箱子的位置表示的狀態是一樣的:
    ( ( 1 , 2 ) , ( 3 , 7 ) , ( 2 , 5 ) ) 和 ( ( 2 , 5 ) , ( 1 , 2 ) , ( 3 , 7 ) ) ((1,2),(3,7),(2,5)) 和 ((2,5),(1,2),(3,7)) ((1,2),(3,7),(2,5))((2,5),(1,2),(3,7))
  • 綜合以上兩點,我們可以做如下處理,盡量減少狀態:

1)從小人的初始位置進行一次深度優先搜索(認為除了墻以外其它點都可達),標記所有遍歷到的點;
2)對以上所有深搜遍歷到的點從 1 開始進行編號,所有這些點編號不重復即可,如圖三-2-2所示:

圖三-2-2


3)小人和箱子到達的位置直接用編碼后的數字表示即可,這樣一來,在壓縮成 K 進制數的時候,K的值從原來的 R x C = 64 變成了 24,大大縮小了狀態空間,狀態編碼的時候,為了讓解碼的時候能夠準確解出有多少個箱子,編碼位從1開始(如果從0開始,那么一旦有個箱子在0的位置會產生二義性),
4)箱子的編碼還是遵從最小表示法,按照遞增順序排完序再壓縮到一維,

  • 由于箱子和人都不能重疊,對于這個關卡來說,23個空位置,選出4個位置放箱子,再從19個位置選擇1個放小人,所以總的狀態數是:
    C 23 4 C 19 1 = 4037880 C_{23}^4C_{19}^1 = 4037880 C234?C191?=4037880
  • 然而還有很多狀態其實是非法的,這里說的非法是指:一旦達到這個狀態,就不可能讓 n 個箱子歸位,比如把某個箱子推到一個不是目標位置的角落里面,如圖三-2-3所示,右下角的箱子這么一放,永遠都無法推到目標點了,

在這里插入圖片描述

圖三-2-3

  • 所以排除這些非法狀態后,實際狀態數會更少,
  • 當然,這只是針對這個資料的,狀態數會隨著地圖的增大、箱子的增多而成倍增長,

5)搜索

  • 狀態確定以后,就可以進行廣度優先搜索了,
  • 分成三步走:

1. 初始狀態生成

  • 對初始的人和箱子進行編碼,就生成了初始狀態,初始狀態壓入佇列;

2. 狀態擴展

  • 從當前佇列首部彈出一個元素,然后進行狀態擴展,對于狀態擴展可以這樣描述:

任何情況下,小人都是選擇往四個方向走一格,對于每個方向,都有3種情況:
1)前面沒路(墻或者越界),這種情況無法擴展狀態;
2)前面沒有箱子,直接往前走,擴展狀態(塞入佇列尾部);
3)前面有個箱子,又分兩種情況:
? 3.1)箱子前面無法放置,這種情況無法擴展狀態;
? 3.2)箱子前面可以放置,人和箱子同時往這個方向前進一格,擴展狀態(塞入佇列尾部);

3. 結束狀態判定

  • 當彈出的元素,進行解碼后發現,四個箱子都已經到了規定的位置,則搜索結束,

四、編碼實作

1、類的定義

  • 定義一個 PushBoxGame 類,成員變數全部定義成私有;
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_;                   // 搜索的最終狀態
};

  • 介面的定義較多,最后會有一個總結,這里為了描述清晰暫且就不列了,只列出必要的成員變數;

2、輸入合法性判定

  • 為了避免狀態過多,短時間搜索不出正確的解,需要對輸入資料進行一個合法性判定,包括以下幾點:

1)地圖大小不超過 10X10
2)箱子最多個數 6
3)目標位置必須 和 箱子數匹配
4)必須嚴格有一個"小人"

  • C++ 代碼實作如下:
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;
}

3、深搜實作格子編號

  • 用深度優先搜索來對格子進行編號;

1)初始化所有格子的標記為 VT_UNVISITED(-1)
2)從 “小人” 位置出發,遍歷所有與之相鄰的連通塊,將遍歷到的塊標記位 VT_VISITED(0);
3)按照從左往右,從上往下的順序對所有已訪問格子進行不重復編號;

1)標記格子未訪問

  • 對格子訪問狀態定義列舉如下:
enum VisitType {
    VT_UNVISITED = -1,
    VT_VISITED = 0,
};
  • 格子訪問狀態存盤在 id_[MAXN][MAXN] 這個成員變數陣列中,然后呼叫 memset 進行統一初始化:
	memset(id_, VisitType::VT_UNVISITED, sizeof(id_));

2)深搜訪問所有連通塊

  • 實作介面 genId_floodfill 尋找小人位置,呼叫 genId_dfs 進行深搜;
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)遞回處理相鄰四個格子

  • C++ 代碼實作如下:
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]);
    }
}

3)對訪問到的塊進行重編號

  • 按照從左往右,從上往下的順序對所有已訪問格子進行重新編號;
  • 并且,增加一個反查表 idrow_ 和 idcol_,通過編號需要知道它原本的行列;
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);
}

4)介面封裝

  • 把呼叫關系封裝起來就是:
void PushBoxGame::genId() {
    genId_floodfill();
    genId_genTerr();
}
  • 在外部看來,只需要呼叫 genId(),就可以實作格子重編號了,

4、實作哈希函式

  • 狀態編碼較大的時候,陣列覆寫不到,所以需要進行取模后映射,
  • 取模帶來的問題就是有可能產生哈希沖突,
  • 所以需要實作哈希沖突后重映射的問題,我用的是 二次尋址法
  • 為了簡化問題,這里采用靜態哈希陣列,如果有興趣的童鞋可以自己實作一下 rehash 和 漸進式 rehash,

1)設計一個哈希類

  • 由于哈希陣列比較大,不適合放在成員變數里,所以把它放在堆上;
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);
};

2)初始化哈希陣列

  • 注意申請記憶體前先判空,避免記憶體泄漏;
  • 類析構的時候呼叫 finalize 進行堆記憶體釋放;
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));
}

3)狀態哈希映射實作

  • getKey 這個函式的含義是:根據狀態 val 的值,在陣列 hashkey_ 中找到一個下標與之一一映射;
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;
        }
    }
}
  • 1)很多開源代碼中,哈希陣列的長度都是 2 的冪,原因有兩個:

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;
}

4)狀態編碼實作

  • 對狀態的編碼三 - 2-4)中提到的狀態壓縮的方法,把所有的數字都壓縮到一個整數上,所以主要對外提供兩個介面:
    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_;
};
  • 實作比較簡單,這里就不貼了,文章結尾會提供整套代碼實作的鏈接;

5、廣搜模擬推箱子程序

  • 廣搜的整個程序寫成偽代碼如下:
bool PushBoxGame::bfs() {
    bfs_initialize();
    bfs_pushInitState();
    while(!queue.empty()) {
        bfs_popFrontState();
        bfs_checkFinalState();
        bfs_extendState();
    }
}
  • 實際C++代碼實作如下:
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;
}

6、路徑回溯

  • 在廣搜進行擴展狀態的時候,我們通過一個鏈表把當前狀態和前驅狀態串聯起來,這樣,最終就可以通過結束狀態回溯到開始狀態了,

7、效果渲染

  • 效果渲染就是從開始狀態到結束狀態,遍歷所有的狀態,然后分別對狀態進行解碼填充地圖即可,
  • 以上所有代碼均可以在我的 github 上找到:推箱子游戲原始碼

五、寫在最后

  • 好了!實用搜索小技巧,你學廢了嗎?提前預告下,下篇介紹將 推箱子 的人機對戰,敬請期待! 《-_-》

  • 最后來看一波機器人的表演吧!
    在這里插入圖片描述
    在這里插入圖片描述

    在這里插入圖片描述
    在這里插入圖片描述
    在這里插入圖片描述
    在這里插入圖片描述
    在這里插入圖片描述
    在這里插入圖片描述
    在這里插入圖片描述

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/240127.html

標籤:其他

上一篇:unity 常用的設計模式

下一篇:Unity3D游戲開發之類物件池優化秘籍殘篇

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more