【算法解决实际问题】BFS解决益智游戏

前言

小时候家里大人不让玩手机,所以我常玩的是华容道益智游戏,一关就要解很久,解不开也没心思去做别的事了。如果用算法解决实际生活中的问题,那么我第一个想解决的是快速通关华容道。
华容道是在一个拼图中有一个格子是空的,利用这个空着的格子去移动其他的滑块(卒、张飞、赵云、关羽等),最后顺利让曹操从中间的空格出来。以第二关七步成诗进行操作,最初,没有用考虑最少步数完成,如图:

这种游戏一般都有一些套路,类似于魔方还原公式,使用的是一些技巧,我们不研究这个令人脑壳痛的技巧,学了算法后知道这个可以使用快乐无比的暴力搜索算法解决————BFS算法框架解决类似的益智游戏。

BFS算法框架

BFS核心思想是把一些问题抽象成图,从一个点开始,像四周扩散,找到到终点最近的距离。常使用的是队列这个数据结构,每次将一个节点周围的所有节点加入队列。

BFS算法框架

//计算从起点到终点最近距离
int BFS(Node start,Node target){
Queue<Node> q;//队列q,核心数据结构
Set<Node> visited;//避免走回头路
q.offer(start);//将起点加入队列
visited.add(start);
int step=0;//记录扩散的步数
while(q not empty){
	int sz=q.size();
	/*将当前队列所有的节点像四周扩散*/
	for(int i=0;i<sz;i++){
		Node cur =q.poll();
		/*划重点:这里判断是否到达终点*/
		if(cur is target)
			return step;
		/*将cur的相邻界点加入队列*/
		for(Node x:cur.adj()){
			if(x not in visited){
				q.offer(x);
				visited.add(x);
			}
		}
		/*划重点:更新步数*/
		step++;
	}
}

cur.adj()泛指cur相邻的节点,比如说二维数组中,cur上下左右四面的位置,就是相邻的节点。
visited的主要作用是防止走回头路。

BFS与DFS

DFS用递归的形式,用到了栈结构,先进后出。
BFS选取状态用队列的形式,先进先出。
DFS的复杂度与BFS的复杂度大体一致,不同之处在于遍历的方式与对于问题的解决出发点不同。
一般来说哈,在找最短路径的时候使用BFS,其他时候DFS偏多,主要是递归代码好写叭。

双向BFS优化

传统的BFS框架是从起点开始向四周扩散,遇到终点时停止。而双向BFS则是从起点和终点同时开始扩散,当两边有交集的时候停止。双向BFS还是遵循BFS算法框架的,只是不再使用队列,而是使用HashSet方便快捷判断两个集合是否有交集。while循环的最后交换q1和q2的内容,所以只要默认扩散q1就相对轮流扩散q1和q2。
不过双向BFS也有局限性,必须得先知道终点在哪里。

问题分析

可以将其类比为滑动拼图问题,leetcode第773题滑动谜题就是滑动拼图问题,以此题为例,进行演算分析。
题目描述如下:

思路描述

这是计算最小步数的问题,正如之前所说,遇到这类问题,最先思考BFS算法。而这个题目转化为BFS的时候,我们会面临这样的问题:

  1. 一般的BFS算法时从一个起点到终点进行寻路的,拼图问题是在不断的交换数字。
  2. 假设这个问题可以转化为BFS问题,那么起点与终点该如何处理?把数组放入队列是个麻烦低效的事情。
    BFS是一种暴力搜索算法
    解决第一个问题:只要涉及暴力穷举的问题,BFS就可以用,并且可以最快的找到最优解。可以将第一个问题转化为“怎么样穷举board当前局面下可能衍生出的所有局面?”可以以数字0为基准,将0和上下左右的数字进行交换就可以了:

    每次先找到数字0,然后和周围数字进行交换,形成新的局面加入队列,当第一次抵达终点时,就以最少步数赢得游戏。
    解决第二个问题:这是一个2x3的二维数组,可以压缩为一个一维的字符串。而二维数组有上下左右的概念,压缩为一维后,获得上下左右的索引成为了一个难点。可以手动写出来这个映射,如下:
vector<vector<int>> neighbor={
  {1,3},
  {0,4,2},
  {1,5},
  {0,4},
  {3,1,5},
  {4,2}
};

这个含义是在一维字符串中,索引i在二维数组中的相邻索引为neighbor[i]:

eg. negihbor[4]={3,1,5}

这两个问题解决后,就可以套用之前讲到的BFS算法框架(套路):

int slidingPuzzle(vector<vector<int>>& board) {
    int m = 2, n = 3;
    string start = "";
    string target = "123450";
    // 将 2x3 的数组转化成字符串
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            start.push_back(board[i][j] + '0');
        }
    }
    // 记录一维字符串的相邻索引
    vector<vector<int>> neighbor = {
        { 1, 3 },
        { 0, 4, 2 },
        { 1, 5 },
        { 0, 4 },
        { 3, 1, 5 },
        { 4, 2 }
    };

    /******* BFS 算法框架开始 *******/
    queue<string> q;
    unordered_set<string> visited;
    q.push(start);
    visited.insert(start);
    int step = 0;
    while (!q.empty()) {
        int sz = q.size();
        for (int i = 0; i < sz; i++) {
            string cur = q.front(); q.pop();
            // 判断是否达到目标局面
            if (target == cur) {
                return step;
            }
            // 找到数字 0 的索引
            int idx = 0;
            for (; cur[idx] != '0'; idx++);
            // 将数字 0 和相邻的数字交换位置
            for (int adj : neighbor[idx]) {
                string new_board = cur;
                swap(new_board[adj], new_board[idx]);
                // 防止走回头路
                if (!visited.count(new_board)) {
                    q.push(new_board);
                    visited.insert(new_board);
                }
            }
        }
        step++;
    }
    return -1;
    /******* BFS 算法框架结束 *******/
}

结果

回到我们最初想要实现最少步数完成第二关,利用我们上面所讨论的BFS算法思想,在实际应用(微信小程序——经典三国华容道)上实践后得到七步完成第二关——七步成诗。如图:

再以实际应用(应用市场————数字华容道快应用)进行测试,未使用算法和使用算法对比后,可见明显差别,如图所示:

备注

文章书写时结合了labuladong博主有关BFS的算法描述,在问题问分析和思路描述是看了许多博客资料,自己理解后的想法。实践结果可能存在一定误差,这是因为数字华容道的每次测试,开局所给的拼图是随机的,可能存在细微的差距。

posted @ 2021-04-16 16:09  XIAOGUAI9  阅读(549)  评论(0)    收藏  举报