179 八数码(单向bfs搜索,双向广搜,A*算法优化)

1. 问题描述:

在一个 3×3 的网格中,1∼8 这 8 个数字和一个 X 恰好不重不漏地分布在这 3×3 的网格中。
例如:
1 2 3
X 4 6
7 5 8
在游戏过程中,可以把 X 与其上、下、左、右四个方向之一的数字交换(如果存在)。我们的目的是通过交换,使得网格变为如下排列(称为正确排列):
1 2 3
4 5 6
7 8 X
例如,示例中图形就可以通过让 X 先后与右、下、右三个方向的数字交换成功得到正确排列。交换过程如下:
1 2 3   1 2 3   1 2 3   1 2 3
X 4 6   4 X 6   4 5 6   4 5 6
7 5 8   7 5 8   7 X 8   7 8 X
把 X 与上下左右方向数字交换的行动记录为 u、d、l、r。现在,给你一个初始网格,请你通过最少的移动次数,得到正确排列。

输入格式

输入占一行,将 3×3 的初始网格描绘出来。例如,如果初始网格如下所示:
1 2 3 
x 4 6 
7 5 8 
则输入为:1 2 3 x 4 6 7 5 8

输出格式

输出占一行,包含一个字符串,表示得到正确排列的完整行动记录。如果答案不唯一,输出任意一种合法方案即可。如果不存在解决方案,则输出 unsolvable。

输入样例:

2  3  4  1  5  x  7  6  8 

输出样例

ullddrurdllurdruldr
来源:https://www.acwing.com/problem/content/description/181/

2. 思路分析:

① 分析题目可以知道这道题目属于最小步数模型的题目:每一个棋盘看成是一个状态,通过交换空格位置与其上下左右四个位置的元素得到其他的状态,我们需要求解从初始状态到目标状态的最短距离并且输出最短距离对应的任何一个方案。最容易想到的是单向的bfs,从初始状态出发一直搜索到目标状态,由于这道题目只有9个元素所以使用单向的bfs搜索也是可以过的,使用单向bfs的一个好处是思维难度不大,有基本的搜索套路,而且记录最短距离对应的方案也是比较方便的,一般可以使用哈希表来记录,因为使用的是python语言所以可以使用一个字典进行记录,字典中的值需要记录两个值,第一个为到达当前状态的上一个状态,第二个是上一个状态到达当前状态对应的操作,因为是bfs搜索,所以第一次搜索到的状态肯定与初始状态的距离是最短的,所以在入队之前就可以判断入队的状态是否是终点状态,如果是终点状态那么说明最短距离已经求解出来了,根据记录的方案的信息逆推到初始状态,最终将方案翻转一遍就是最短距离对应的方案。对于最小步数模型的题目一般状态空间是非常大的,这道题目的状态空间没有那么大所以使用单向的bfs搜索也是可以通过的,如果搜索的空间非常大,那么可以使用双向广搜或者是A*算法进行优化,双向广搜比较常用的实现方式是从初始状态向目标状态搜索和从目标状态向初始状态搜索,如果题目是有解的那么最终在搜索的时候会相遇在某一个之前某一个方向上已经搜索过的状态,当相遇的时候说明最短距离就已经求解出来了,如果只是求解最短距离代码其实是很好写的,如果要记录最短距离的方案那么就比较麻烦了,因为从终点向起点搜索的时候操作序列是相反的,所以当搜索到的状态重合的时候需要判断属于从哪一个方向向哪一个方向搜索,起始在写代码的时候可以用字典来记录当当前状态的操作序列,而不是只是简单记录到达当前状态的前一个状态是什么,最终在相遇的时候判断属于哪一种情况对终点向起点搜索的方案进行翻转。除了双向广搜之外,起始还可以使用A*算法进行优化。

② A*算法,与双向广搜时候类似的,都是针对于搜索空间非常大的bfs搜索问题,A*算法在实现的时候需要使用一个启发函数进行优化,启发函数可以使得从起始状态开始搜索的时候搜到的比较少的状态就可以搜索到目标状态,这也是A*算法的应用场景,只有当搜索空间非常大的时候A*算法才是有效的,而且A*算法一般使用在有解的题目,对于无解的题目直接使用单向的bfs搜索都比A*算法更快,A*算法的实现步骤:将普通的bfs的队列换成优先队列,也即小根堆,队列中存储两个值,第一个值为初始状态到达当前状态的距离 + 使用启发函数估计的从当前状态到目标状态的估价距离,第二个值为当前的状态,当终点第一次出队的时候说明找到了从初始状态到目标状态的最短距离,在扩展的时候选择优先队列中的队头元素进行扩展,遍历当前状态可以转换的其他状态,如果之前没有遍历过或者是通过队头元素扩展的距离更短那么就更新,并且将其加入到优先队列中。保证A*算法正确的一个条件是:d(state) + f(state) <= d(state) + g(state) ,f为计算state到目标状态估价距离的启发函数,g为state到终点状态的真实距离,d为初始状态到state的真实距离。对于八数码问题来说有解的充分必要条件是逆序对的个数为偶数(充分性很难证明必要性比较好证明),我们可以先统计一下初始状态逆序对的个数,如果逆序对的个数为奇数说明无解,如果有解那么使用A*算法即可。对于这道题目的启发函数可以计算从当前每一个数到这个数的真实位置的曼哈顿距离。可能这道题目的搜索空间不是特别大所以使用A*算法没有什么优化效果,单向的bfs比A*算法更快。

3. 代码如下:

单向bfs(使用一个数据结构pre(一般是字典,记录前一个状态和对应的操作序列)记录到达当前状态的前一个状态):

import collections


class Solution:
    def swap(self, a: int, b: int, state: str):
        s = list(state)
        s[a], s[b] = s[b], s[a]
        return "".join(s)

    # 使用普通的bfs来搜索, 单向的bfs搜索的时候一般使用一个数据结构pre来记录到达当前状态的前一个状态, 最后逆推即可, 单向bfs的思维难度比较小都是一般的套路
    def bfs(self, start):
        end = "12345678x"
        q = collections.deque([start])
        dis, pre = dict(), collections.defaultdict(list)
        # 起点的距离为0
        dis[start] = 0
        pos = [[0, 1], [0, -1], [1, 0], [-1, 0]]
        op = "rldu"
        while q:
            state = q.popleft()
            x = y = 0
            for i in range(len(state)):
                if state[i] == "x":
                    x, y = i // 3, i % 3
                    break
            for i in range(4):
                a, b = x + pos[i][0], y + pos[i][1]
                if 0 <= a < 3 and 0 <= b < 3:
                    # 交换两个位置的元素
                    r = self.swap(a * 3 + b, x * 3 + y, state)
                    if r not in dis:
                        dis[r] = dis[start] + 1
                        pre[r] = [state, op[i]]
                        q.append(r)
                        res = ""
                        if r == end:
                            # 逆推方案
                            while start != end:
                                res += pre[end][1]
                                end = pre[end][0]
                            return res[::-1]
        return ""

    def process(self):
        s = input()
        start, end = "", "12345678x"
        if start == end:
            print("")
            return
        seq = ""
        for c in s:
            if c != " ":
                if c != "x": seq += c
                start += c
        count = 0
        for i in range(len(seq)):
            for j in range(i + 1, len(seq)):
                if seq[i] > seq[j]: count += 1
        if count & 1:
            print("unsolvable")
        else:
            print(self.bfs(start))


if __name__ == '__main__':
    Solution().process()

双向广搜(记录方案):

import collections
from typing import List


class Solution:
    def swap(self, a: int, b: int, state: str):
        # 转换为列表才可以交换
        s = list(state)
        # 交换state中对应一维位置的元素
        s[a], s[b] = s[b], s[a]
        # 将列表转换为字符串
        return "".join(s)

    # 字典da中第一个元素为到当前状态的最短距离, 第二个为到达当前状态的操作序列(直接存储在字典中会更容易处理), 双向搜索记录方案会比较麻烦一点, 这里直接记录的是操作序列
    def extend(self, q: List[str], da: collections.defaultdict, db: collections.defaultdict, op: str):
        d = da[q[0]][0]
        pos = [[0, 1], [0, -1], [1, 0], [-1, 0]]
        # 最外层的循环用来扩展同一深度的所有元素
        while q and da[q[0]][0] == d:
            t = q.pop(0)
            # 先找到字符串中x的位置
            x = y = 0
            for i in range(9):
                if t[i] == "x":
                    x, y = i // 3, i % 3
                    break
            # 扩展(x, y)周围的四个方向, 也即交换空格与周围的元素位置
            for i in range(4):
                a, b = x + pos[i][0], y + pos[i][1]
                # 越界
                if a < 0 or a >= 3 or b < 0 or b >= 3: continue
                r = self.swap(x * 3 + y, a * 3 + b, t)
                if r in da: continue
                # 判断属于哪一种情况: 终点到起点还是起点到终点的情况, 可以根据op可以判断出属于哪一种情况即可判断, 返回对应的字符串即可
                if r in db:
                    if op == "rldu":
                        return da[t][1] + op[i] + db[r][1][::-1]
                    else:
                        return db[r][1] + op[i] + da[t][1][::-1]
                # da[r]值中的第二个元素表示到当前状态的操作序列
                da[r] = [da[t][0] + 1, da[t][1] + op[i]]
                q.append(r)
        return -1

    def bfs(self, start):
        end = "12345678x"
        if start == end: return 0
        qa, qb = [start], [end]
        # 字典需要存储两个值, 第一是到达当前状态的最短距离, 第二个是到达当前状态的操作序列
        da, db = collections.defaultdict(list), collections.defaultdict(list)
        da[start] = [0, ""]
        db[end] = [0, ""]
        # op1表示右/左/下/上, 对应rldu, op2表示左/右/上/下, 对应rldu
        op1, op2 = "rldu", "lrud"
        while qa and qb:
            t = 0
            if len(qa) <= len(qb):
                t = self.extend(qa, da, db, op1)
            else:
                # 从终点开始搜索方向是相反的, 所以需要传入op
                t = self.extend(qb, db, da, op2)
            if t != -1:
                return t

    # 可以使用双向广搜来解决, 属于最小步数模型(每一个棋盘看成是一个状态)
    def process(self):
        s = input()
        start, seq = "", ""
        for c in s:
            if c != " ":
                if c != "x": seq += c
                start += c
        # 计算逆序对的个数
        count = 0
        for i in range(len(seq)):
            for j in range(i + 1, len(seq)):
                if seq[i] > seq[j]: count += 1
        # 逆序对的个数为奇数所以无解
        if count & 1:
            print("unsolvable")
        # 有解
        else:
            print(self.bfs(start))


if __name__ == '__main__':
    Solution().process()

A*算法:

import heapq


class Solution:
    # 计算估价距离
    def f(self, state: str):
        s = 0
        for i in range(len(state)):
            if state[i] != "x":
                c = int(state[i])
                s += abs(i // 3 - c // 3) + abs(i % 3 - c % 3)
        return s
    
    # 交换两个位置的字符
    def swap(self, a: int, b: int, state: str):
        s = list(state)
        s[a], s[b] = s[b], s[a]
        return "".join(s)

    def bfs(self, start: str):
        # 存储估价距离与真实距离
        dis = dict()
        dis[start] = 0
        end = "12345678x"
        # 存储到达当前状态的前一个状态
        pre = dict()
        q = list()
        # heapq相当于小根堆, 第一个元素是估价距离, 第二个元素当前的状态
        pos = [[0, 1], [0, -1], [1, 0], [-1, 0]]
        op = "rldu"
        heapq.heappush(q, (self.f(start), start))
        while q:
            p = heapq.heappop(q)
            if p[1] == end: break
            # 先找到"x"的位置
            state = p[1]
            x = y = 0
            for i in range(len(state)):
                if state[i] == "x":
                    x, y = i // 3, i % 3
                    break
            # 交换上下左右四个方向
            for i in range(4):
                a, b = x + pos[i][0], y + pos[i][1]
                # 越界了
                if 0 <= a < 3 and 0 <= b < 3:
                    r = self.swap(x * 3 + y, a * 3 + b, state)
                    if r not in dis or dis[r] > dis[state] + 1:
                        dis[r] = dis[state] + 1
                        # 预估距离 + 到当前这个状态的预估距离
                        heapq.heappush(q, (dis[r] + self.f(r), r))
                        pre[r] = [state, op[i]]
        res = ""
        # 逆序推导最短路径的方案
        while end != start:
            res += pre[end][1]
            end = pre[end][0]
        return res[::-1]

    def process(self):
        s = input()
        start, seq = "", ""
        for c in s:
            if c != " ":
                if c != "x":
                    seq += c
                start += c
        count = 0
        for i in range(len(seq)):
            for j in range(i + 1, len(seq)):
                if seq[i] > seq[j]: count += 1
        if count & 1:
            print("unsolvable")
        else:
            # 有解
            print(self.bfs(start))


if __name__ == '__main__':
    Solution().process()

猜你喜欢

转载自blog.csdn.net/qq_39445165/article/details/121687048
179