八数码难题——BFS

301 阅读6分钟

问题描述

3×3 九宫棋盘,放置数码为1 -8的8个棋牌,剩下一个空格。指定初始状态和目标状态,要求只能通过移动空白格来形成指定棋盘。求最短步数以及移动方法。

 

核心问题:逆序数

在八数码问题中,两个状态的逆序数奇偶性相同,他们之间才可达

在这个题目中,初始状态和目标状态给定且逆序数都为奇,所以初始状态是合法的。同样的,在后续尝试每一个状态时,都需要判断该状态逆序数是否为奇数,若是,则可以对该节点继续扩展,否则该节点是非法节点,一定不可能移动到目标状态。

没进行一步就求一次逆序数是不是很耗时呢?实际上,在八数码问题中这个操作是多余的。原因很简单:首先在对矩阵编码时,我们把空白格看作是数字0,则状态序列是一个从0到8的序列,但要注意计算逆序数时是不把0元素包含在内的!它只是一个空白格位置的标记。

0元素在不出界的前提下总共有上下左右四种走法

  • 往左、右移动:数列1-8的相对位置没有任何变化,所以数列的逆序数不变。
  • 往上、下移动:空白格在序列0-8中往前(后)移动了3个单位距离,相当于将一个数字X在数列1-8中向前(后)移动2格(3-1=2),那么跳过的这两个数,要么比数字X都要大(小),逆序数可能±2;要么一个比数字X大,一个比数字X小,逆序数不变(+1和-1正好抵消)。所以逆序数仍然不会改变

 

由以上可知:在八数码问题中,无论空白格怎么移动,状态逆序数的奇偶性都不会改变。但不保证在n数码问题中也有这样的现象。例如4*4棋盘,在上下移动空白格时,逆序数会±3或者±1,其奇偶性必然发生改变。

 

BFS解体思路

  1. 判断初始状态是否和目标状态一致。若一致,程序结束;否则,转入步骤2。
  2. 判断初始状态(自由输入)的逆序数奇偶性是否和目标状态一致。若一致,转入步骤3;否则,程序结束。
  3. 状态入队,进入步骤4
  4. 如果队为空,则结束程序;若非空,则首元素(状态)出队并找到该状态下0元素的位置,分别分析四个方向的可行性。如果当前方向可行,则进入步骤5;否则继续寻找下一个方向,如果四个方向寻找完毕,则进入步骤9
  5. 已找到可行的位置to,执行 swap(当前状态[zero],当前状态[to]),即交换位置。进入步骤6.
  6. 判断该状态是否已经分析过(有标记),如果已经处理过,则进入步骤8;否则进入步骤7
  7. 判断该状态是否为目标状态,若是,则输出搜索深度cnt[head]+1作为最终结果并结束整个程序;若不是则执行cnt[tail++] = cnt[head] + 1并将该状态如队并作标记。进入步骤8 。
  8. 执行swap(当前状态[zero],当前状态[to])。进入步骤9
  9. head++ 进入步骤4。

 

难点

标记深度

 

小思考:给字符串做标记

我们知道:每一个处理过的状态都不能再次分析。每一个状态都是一个字符串形式,那怎么才能耗最少的空间标记这个string并用最少的时间查找呢?首先想到的就是存到一个大集合里然后每次查找,太蠢肯定不行。那用索引?但字符串不能当作下标啊!有办法吗?其实是有的。在c++中,可以使用map<string,bool>m;的方法,如果出现状态now,则标记m[now]=True,然后使用if(!m[now])判断即可。我们得到启发:这就是借用键值对的功能啊,用字符串作为键,用True作为值,这不就可以高效得实现对字符串的标记吗。 那在python中怎么用呢?

python中只有字典支持键值存储,那我们就大胆使用,处理过的标记为True,未处理的标记False。但这样真的高效吗?在判断时我们有必要每一个都区分True和False吗?其实我们只需要把出现过的字符串标记为True,没出现的不用管,在判断时只判断这个键(字符串)在字典中是否存在即可,而不用管它的值是什么,这样就减少了特别多的空间占用,另一方面,因为不用管键所对应的值是什么,我们完全可以标记字符串为dict[now]='$'之类的,无任何影响。

 

程序实现

import sys
import queue
import numpy as np
np.set_printoptions(threshold=np.inf)  # threshold 指定超过多少使用省略号,np.inf代表无限大

start = None
end = '123804765'
dict = {}  # 用来做状态处理的标记  相当于open表
ans = np.zeros(400000, dtype=int)  # 如果开的小,很多棋盘会解不出来

# 描述空白格移动
def swap(str, zero, to):
    temp = list(str)  # 在python中,字符串是不可变的变量,所以先转化为list
    temp[zero], temp[to] = temp[to], temp[zero]
    now = ''.join(temp)
    return now

# 求初始状态的逆序数判断是否合法
def inversion(str):
    sum = 0
    for i in range(9):  # 0-8
        for j in range(i + 1, 9):
            if str[j] < str[i] and str[j] != '0':  # 不把0算入在内
                sum = sum + 1
    return sum


# 试探空白格移动的四个方向
def move(i, index):
    if i == 1:  # 向上
        if index - 3 >= 0:
            return index - 3
        else:
            return -1
    elif i == 2:  # 向下
        if index + 3 <= 8:
            return index + 3
        else:
            return -1
    elif i == 3:  # 向左
        if index % 3 != 0:
            return index - 1
        else:
            return -1
    elif i == 4:  # 向右
        if index % 3 != 2:
            return index + 1
        else:
            return -1


def BFS(start):
    head, tail, flag = 0, 1, False


    # 步骤3
    q = queue.Queue()  # 括号中是队列可容纳数据的多少,如果不设置,则可以一直增加
    q.put(start)

    # 步骤4
    while not (q.empty() or flag):
        now = q.get()  # 取出的时候顺带弹出了.  now现在是str类型
        zero = now.find('0')  # 寻找0的位置

        # 向四个方向寻找
        for i in range(1, 5):
            to = move(i, zero)

            if to == -1:
                continue

            # 步骤5
            now = swap(now, zero, to)  # 交换

            # 步骤6
            if now not in dict:
                # 步骤7
                if now == end:
                    print(ans[head] + 1, end=' steps')  # 深度
                    flag = True  # 用来跳出 while
                    break        # 用来跳出 for

                ans[tail] = ans[head] + 1
                tail = tail + 1

                dict[now] = i  # 该状态已分析过,做标记
                q.put(now)

            # 步骤8
            now = swap(now, zero, to)  # 恢复现场

        # 步骤9
        head = head + 1


if __name__ == '__main__':

    # start = input()  # 输入初始状态
    start = '216408753'  # 283104765:4     283164705:5   216408753:18   234150768:无解

    # 步骤1
    if start == end:
        print('已是终点状态')
        sys.exit()

    # 步骤2
    if inversion(start) % 2 != inversion(end) % 2:
        print('该状态无解')
        sys.exit()

    BFS(start)