导语

好久没更新,从去年十月份来,工作突然很忙,平时剩下的时间用来学习、健身,给到总结写作,没剩下时间。加上人每隔一段时间,都会产生懈怠,就停更个人技术博客近半年。想想去年因为疫情在家办公期间,某种程度上来看,还是很幸福的,上班不用来回坐车,节省下来的时间,可以花在自己感兴趣的事情上,产生一些输出。

OK,言归正传,今天闲来无事,我翻出了女儿去年玩过的小游戏,她妈妈给她买的这类智力小游戏有不少,很偶尔会瞧见她们娘俩一起玩,估计是不太感兴趣。今天这款游戏,我小时候没见过,是一个新的小游戏,我就起一个应景的名字,《兔兔大战小狐狸》,今年是兔年,兔子要逃脱小狐狸的围追堵截,逃到安全的小土坑里,躲起来。

WechatIMG53.jpeg

游戏介绍

游戏棋盘是一个5*5的方盘,左上、右上、左下、右下以及正中间有5个小土坑,如果棋盘上所有的兔子,都在土坑待着,就完成了任务。具体规则,我不码字,看如下说明:
WechatIMG51.jpeg

如上面所说,游戏内置了60个关卡(因为随便摆放,是不一定能完成任务的),也有最少步骤的解答,我自己尝试玩了几个简单关卡(最少10步以内可以完成的),貌似还挺简单的。

然后,我翻到了关卡的最后4个关卡,哇,一下子震惊了,好难,最后一关官方解答,需要87步完成任务。
WechatIMG50.jpeg

WechatIMG49.jpeg

一些疑问

小朋友如果自己完成前面简单、中等题目,已经很不容易了。至于困难、专家级别的题目,如果能完成,小脑瓜不是一般聪明,非常了不起。但我想了下,官方说最后一题87步是最少步骤,这个怎么证明官方没错呢?或者有更少步骤的解法?我心想,是不是写个算法求解,看看是否是对的。

求解过程

首先是需要有个建模过程,棋盘规格是5*5方盘,上面有25个位置,5个位置是土坑。可以放置的角色是小狐狸、兔兔和蘑菇。兔兔和蘑菇都只占用一个格子,狐狸占用两个格子,还有头和尾巴,但我仔细想了下,狐狸头和尾巴,其实在算法中不需要区分,只需要区分狐狸是竖着还是横着放置的,你可以理解为狐狸的头总是在左边或者上方,尾巴在右边或者下方。这样对算法实现更简单。

比如60题,我会使用5个字符串表示5行,

"...X."
"FF.R."
"..X.R"
"X...."
"...R."

字符X表示蘑菇,F表示小狐狸(头或者尾),R表示小兔兔,最后.表示空位。

算法显然就是搜索算法,DFS还是BFS呢?我先用DFS实现了半天,会TLE,根本算不出最少步骤的解法。我后来想了下,最少步骤的解法确实不太适合DFS,因为DFS可能需要搜索完所有的解法空间(也可能我剪枝还不够),所以无法算出最少步骤。在DFS上,我其实花了不少时间优化,这个题目比刷题难,因为是自己出给自己的,也没有什么参考答案,都只能自己去想,如何建模和实现搜索算法,总之,自己试了半天,也不work。

OK,那么我就来试下BFS搜索,大概的逻辑步骤如下:

1. 建立一个队列,将游戏初始棋盘局面放到队列中;
2. 一一取出当前局面,评估可能变为的下一个局面;
3. 评估当前局面可能可以行动的小兔兔、小狐狸坐标;
4. 一一取出小兔兔、小狐狸,评估她们可能的移动到的位置;
5. 通过改变小兔兔、小狐狸位置,得到下一个局面;
6. 将2~5变化的到的局面插入队列(下一轮);
7. 遍历完上一轮队列,步骤+1;
8. 把下一轮队列变为需要遍历的当前队列,重复2~7步骤;

代码实现:

int smartGame(vector<string> board) {
    int n = board.size();

    // 判断游戏是否结束
    set<vector<int>> finish_pos = {{0, 0}, {0, n-1}, {n-1, 0}, {n-1, n-1}, {n/2, n/2}};
    function<bool(vector<vector<int>>)> isFinished = [&](vector<vector<int>> ani_pos) -> bool {
        int n = ani_pos.size();

        for (int i = 0; i < n; ++i) {
            if (ani_pos[i][2] != -1) continue;
            vector<int> pos = {ani_pos[i][0], ani_pos[i][1]};
            if (finish_pos.find(pos) == finish_pos.end())
                return false;
        }

        return true;
    };

    // 分析棋盘局面,哪些可能移动的小兔兔、小狐狸所在位置
    function<void(vector<string>, vector<vector<int>>&)> analyzeBoard = [&](vector<string> board, vector<vector<int>>& ani_pos) {
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                if (board[i][j] == 'X') {
                    continue;
                } else if (board[i][j] == '.') {
                    continue;
                } else if (board[i][j] == 'F') {
                    if ((i == 0 || board[i - 1][j] != 'F') && (i < n - 1 && board[i + 1][j] == 'F')) {
                        // vertical fox
                        ani_pos.push_back({i, j, 1});
                    } else if ((j == 0 || board[i][j - 1] != 'F') && (j < n - 1 && board[i][j + 1] == 'F')) {
                        // horizontal fox
                        ani_pos.push_back({i, j, 2});
                    }
                } else if (board[i][j] == 'R') {
                    ani_pos.push_back({i, j, -1});
                }
            }
        }
    };

    // 棋盘局面上某个小兔兔、小狐狸,做出某个移动后,返回新的局面
    function<vector<string>(vector<string>, vector<int>, vector<int>)> applyBoard = [&] (vector<string> board, vector<int> pos, vector<int> move) -> vector<string> {
        if (pos[2] == -1) {
            board[pos[0]][pos[1]] = '.';
            board[pos[0]+move[0]][pos[1]+move[1]] = 'R';
        } else {
            if (pos[2] == 1) {
                board[pos[0]][pos[1]] = '.';
                board[pos[0]+1][pos[1]] = '.';

                board[pos[0]+move[0]][pos[1]+move[1]] = 'F';
                board[pos[0]+move[0]+1][pos[1]+move[1]] = 'F';
            } else if (pos[2] == 2) {
                board[pos[0]][pos[1]] = '.';
                board[pos[0]][pos[1]+1] = '.';

                board[pos[0]+move[0]][pos[1]+move[1]] = 'F';
                board[pos[0]+move[0]][pos[1]+move[1]+1] = 'F';
            }
        }

        return board;
    };

    // 分析棋盘局面,可能移动的小兔兔、小狐狸,能做出哪些移动
    function<vector<vector<int>>(vector<string>, vector<int>)> nextMoves = [&] (vector<string> board, vector<int> pos) -> vector<vector<int>> {
        vector<vector<int>> moves;

        if (pos[2] == -1) {
            // up
            vector<int> t = pos;
            while (t[0] >= 0 && board[t[0]][t[1]] != '.') t[0] -= 1;
            if (t[0] >= 0 && board[t[0]][t[1]] == '.' && t[0] < pos[0]-1) {
                moves.push_back({t[0]-pos[0], 0});
            }
            // down
            t = pos;
            while (t[0] < n && board[t[0]][t[1]] != '.') t[0] += 1;
            if (t[0] < n && board[t[0]][t[1]] == '.' && t[0] > pos[0]+1) {
                moves.push_back({t[0]-pos[0],0});
            }
            // left
            t = pos;
            while (t[1] >= 0 && board[t[0]][t[1]] != '.') t[1] -= 1;
            if (t[1] >= 0 && board[t[0]][t[1]] == '.' && t[1] < pos[1]-1) {
                moves.push_back({0, t[1]-pos[1]});
            }
            // right
            t = pos;
            while (t[1] < n && board[t[0]][t[1]] != '.') t[1] += 1;
            if (t[1] < n && board[t[0]][t[1]] == '.' && t[1] > pos[1]+1) {
                moves.push_back({0, t[1]-pos[1]});
            }
        } else if (pos.size() == 3) {
            if (pos[2] == 1) {
                // up
                if (pos[0] > 0 && board[pos[0]-1][pos[1]] == '.') {
                    moves.push_back({-1, 0});
                }
                // down
                if (pos[0] < n-2 && board[pos[0]+2][pos[1]] == '.') {
                    moves.push_back({1, 0});
                }
            } else if (pos[2] == 2) {
                // left
                if (pos[1] > 0 && board[pos[0]][pos[1]-1] == '.') {
                    moves.push_back({0, -1});
                }
                // right
                if (pos[1] < n-2 && board[pos[0]][pos[1]+2] == '.') {
                    moves.push_back({0, 1});
                }
            }
        }

        return moves;
    };

    // 棋盘局面的签名,用作去重
    function<string(vector<string>)> signature = [&](vector<string> board) -> string {
        string r;
        for (auto l : board) {
            r += l;
        }
        return r;
    };

    // 1. 初始棋盘局面,入到队列
    queue<vector<string>> q;
    unordered_set<string> seen;
    q.push(board);
    seen.insert(signature(board));

    int ans = 0;

    while (!q.empty()) {
        int size = q.size();
        while (size --) {
            // 2. 一一取出当前局面,评估可能变为的下一个局面;
            vector<string> t = q.front();
            q.pop();
            vector<vector<int>> ani_pos;

            // 3. 评估当前局面可能可以行动的小兔兔、小狐狸坐标;
            analyzeBoard(t, ani_pos);

            if (isFinished(ani_pos)) {
                return ans;
            }

            // 4. 一一取出小兔兔、小狐狸,评估她们可能的移动到的位置;
            for (auto pos: ani_pos) {
                vector<vector<int>> moves = nextMoves(t, pos);
                for (int i = 0; i < moves.size(); ++i) {
                    vector<int> move = moves[i];
                    // 5. 通过改变小兔兔、小狐狸位置,得到下一个局面;
                    vector<string> nxtBoard = applyBoard(t, pos, move);
                    string sig = signature(nxtBoard);
                    if (seen.find(sig) == seen.end()) {
                        // 6. 将2~5变化的到的局面插入队列(下一轮);
                        q.push(nxtBoard);
                        seen.insert(sig);
                    }
                }
            }
        }

        // 7. 遍历完上一轮队列,步骤+1;
        // 8. 把下一轮队列变为需要遍历的当前队列,重复2~7步骤;
        ans++;
    }

    return -1;
}

还要强调一点优化,我用了一个unordered_set去做棋盘局面去重,曾经看见过的局面,就不需要再去考虑了,因为即使从该局面出发有解,肯定也不会比之前出现时更优的,这个很好理解吧。

结论

最终通过算法验证,最后4题的最优解确实和官方的步骤数量一样,并且我后来记录了最优解算法的到的步骤,虽然有些顺序和官方题解不一致,我仔细比对了下,仅仅是某些步骤中,可以交换顺序而已,不是计算错误。别看是思路很简单,就是BFS搜索算法,但是面对一个没有参考答案的问题,建模、优化和调试,哪一样都不轻松,很开心,今天可以完成任务,并且写下这篇文章,明天继续加油~

标签: 游戏

已有 26 条评论

  1. 想想你的文章写的特别好

  2. 不错不错,我喜欢看 www.jiwenlaw.com

  3. 哈哈哈,写的太好了https://www.cscnn.com/

  4. 真好呢

  5. 龙啸九天:传奇飞龙,探寻行业?:https://501h.com/fugu/5933.html

  6. 《僵尸福星(粤)》香港剧高清在线免费观看:https://www.jgz518.com/xingkong/141853.html

  7. 哈哈哈,写的太好了https://www.lawjida.com/

  8. 哈哈哈,写的太好了https://www.lawjida.com/

  9. 结论升华部分可联系更高维度价值观。

  10. 瑕不掩瑜,稍加打磨必成佳作。

  11. 幽默外壳包裹严肃内核,寓教于乐。

  12. 这篇文章如同一幅色彩斑斓的画卷,每一笔都充满了独特的创意。

  13. 化身博士

  14. 护肤惊魂

  15. 404宿灵速速逃

  16. 朋友家人和爱人

  17. 开心超人之逆世营救

  18. LOL惊喜冬季时装秀

  19. 米歇尔布托美丽真心话

  20. 夏天和冬天

  21. 零号追杀

  22. 科学怪人的新娘

  23. 新盘新项目,不再等待,现在就是最佳上车机会!

  24. 新盘新项目,不再等待,现在就是最佳上车机会!

  25. 2025年10月新盘 做第一批吃螃蟹的人coinsrore.com
    新车新盘 嘎嘎稳 嘎嘎靠谱coinsrore.com
    新车首发,新的一年,只带想赚米的人coinsrore.com
    新盘 上车集合 留下 我要发发 立马进裙coinsrore.com
    做了几十年的项目 我总结了最好的一个盘(纯干货)coinsrore.com
    新车上路,只带前10个人coinsrore.com
    新盘首开 新盘首开 征召客户!!!coinsrore.com
    新项目准备上线,寻找志同道合的合作伙伴coinsrore.com
    新车即将上线 真正的项目,期待你的参与coinsrore.com
    新盘新项目,不再等待,现在就是最佳上车机会!coinsrore.com
    新盘新盘 这个月刚上新盘 新车第一个吃螃蟹!coinsrore.com

  26. 2025年10月新盘 做第一批吃螃蟹的人coinsrore.com
    新车新盘 嘎嘎稳 嘎嘎靠谱coinsrore.com
    新车首发,新的一年,只带想赚米的人coinsrore.com
    新盘 上车集合 留下 我要发发 立马进裙coinsrore.com
    做了几十年的项目 我总结了最好的一个盘(纯干货)coinsrore.com
    新车上路,只带前10个人coinsrore.com
    新盘首开 新盘首开 征召客户!!!coinsrore.com
    新项目准备上线,寻找志同道合的合作伙伴coinsrore.com
    新车即将上线 真正的项目,期待你的参与coinsrore.com
    新盘新项目,不再等待,现在就是最佳上车机会!coinsrore.com
    新盘新盘 这个月刚上新盘 新车第一个吃螃蟹!coinsrore.com

添加新评论