我的 8 个方格的 BFS 实现有什么问题

问题描述

我设计了一个算法,可以使用 BFS 或 DFS 解决 8 平方问题。当前的问题是它运行了无限长的时间。如果我让它运行 30 分钟左右。它以我的 RAM 得到结束完整。我的实现有什么问题。我没有成功调试这段代码。 提前致谢。

import copy
import time
import pprint
def get_empty_board():
    return [
        [7,4,2],[8,3,0],[1,5,6]
        ]
end_state = [
    [1,2,3],4],[7,6,5]
    ]
def solve_squares(board):
    states_explored = list()
    states = [ (neighbor,[board] + [neighbor]) for neighbor in get_neighbors(board) ]
    while states:
        print(len(states))
        current_state = states.pop(0)
        if (current_state[0]) in states_explored:
            continue
        # if len(states) > 300:
            # states =[ states[0] ]
        # pprint.pprint(current_state)
        # pprint.pprint(current_state)
        if current_state[0] == end_state:
            return True,current_state[1]
        neighbors = get_neighbors(current_state[0])
        states_explored.append(current_state[0])
        for neighbor in neighbors:
            if (neighbor) not in states_explored:
                states.append((neighbor,current_state[1] + [neighbor]))
                states_explored.append((neighbor[0]))
    return False,None
def get_neighbors(board):
    x = None
    y = None
    neighbors = list()
    for i in range(len(board)):
        for j in range(len(board[0])):
            if board[i][j] == 0:
                x = i
                y = j
                break
    # print(x,y)
    for i in range(len(board)):
        for j in range(len(board[0])):
            if abs(i-x) <= 1 and abs(j-y) <= 1:
                if abs(i-x) != abs(j-y):
                    # print(i,j)
                    new_state = copy.deepcopy(board)
                    new_state[x][y] = new_state[i][j]
                    new_state[i][j] = 0
                    # pprint.pprint(new_state)
                    # time.sleep(5)
                    neighbors.append(new_state)
    return neighbors
def main():
    result,path = solve_squares(get_empty_board())
    print(result)
    print(path)
main()

解决方法

您的解决方案需要进行一些改进:

  1. 您正在使用 Python 列表(例如,states_explored)来跟踪您已经访问过的电路板配置。现在,列表对于 x in s 的平均案例复杂度是:O(n)。为此,您需要应用一些有效的数据结构(例如,set)。您可以查看 this stack-overflow answer 以了解有关此优化的详细讨论。
  2. 您的 RAM 已满,因为您将每个发现的电路板配置的完整路径存储在队列中(例如,states)。这是非常不必要的并且内存效率低下。为了解决这个问题,您可以使用有效的数据结构(例如,map)来存储给定状态的父状态。发现目标后,需要通过回溯地图构建路径。

我们可以通过以下示例将第二个优化可视化。假设您将 <state,parent-state> 映射为键值:

<initial-state,NaN>
<state-1,initial-state>
<state-2,state-1>
<state-3,state-2>
...
...
<final-state,state-n>

现在发现final-state后,我们可以查询地图中final-state的父节点是什么。然后,递归地进行这个查询,直到我们到达 initial-state

如果您应用这两项优化,您将在运行时间和内存消耗方面获得巨大改进。

,

问题确实是性能。要了解速度,请将以下 print 放入您的代码(且仅此一个):

    if len(states_explored) % 1000 == 0:
        print(len(states_explored))

您会看到它在进行过程中是如何变慢的。我写了一个更有效的实现,发现算法需要访问超过 100,000 个状态才能找到解决方案。以您看到上述行输出行的速度,您可以想象需要 很长时间 才能完成。我没有耐心等待。

请注意,我在您的代码中发现了一个错误,但它不会损害算法:

states_explored.append((neighbor[0]))

这个说法是错误的,原因有两个:

  • neighbor 是一个棋盘,因此从中获取索引 [0],生成该棋盘的第一行,这对于该算法是无用的。
  • 如果您将其更正为仅 neighbor,它会变得很重要,但会使算法停止运行,因为当这些邻居从队列中弹出时,搜索将停止。

所以这一行应该被省略。

以下是一些提高算法效率的方法:

  • 使用原始值来表示板,而不是列表。例如,一个 9 个字符的字符串就可以完成这项工作。 Python 处理字符串比处理二维列表要快得多。这也意味着您不需要 deep_copy
  • 不要使用列表来跟踪访问了哪些状态。使用一套——或者也涵盖下一点——一本字典。在集合/字典中查找比在列表中查找效率更高。
  • 不要存储通向某个状态的整个电路板路径。跟踪之前的状态就足够了。您可以使用字典来指示访问了某个状态以及它来自哪个状态。这将把路径表示为一个链表。因此,一旦找到目标,您就可以从中重建路径。
  • 寻找邻居时,不要迭代板的每个单元格。这些邻居在哪个索引上很清楚,最多有4个。只需定位这四个坐标并检查它们是否在范围内。在这里,棋盘的字符串表示也将派上用场:您可以使用 board.index("0") 定位 0 单元格。
  • 不要在列表上使用 .pop(0):它效率不高。您可以改用 deque。或者 - 在这种情况下我更喜欢 - 根本不流行。相反,使用两个列表。迭代第一个,并填充第二个。然后将第二个列表分配给第一个列表,并使用空的第二个列表重复该过程。

这是我建议的代码。它具有我在本答案开头所建议的相同 print,并在几秒钟内找到了解决方案。

def get_empty_board():
    return "742830156"

end_state = "123804765"

def print_board(board):
    print(board[:3])
    print(board[3:6])
    print(board[6:])

def solve_squares(board):
    states = [(board,None)]
    came_from = {}
    while states:
        frontier = []
        for state in states:
            board,prev = state
            if board in came_from:
                continue
            came_from[board] = prev
            if len(came_from) % 1000 == 0:
                print(len(came_from))
            if board == end_state:  # Found! Reconstruct path
                path = []
                while board:
                    path += [board]
                    board = came_from[board]
                path.reverse()
                return path
            frontier += [(neighbor,board) for neighbor in get_neighbors(board) if neighbor not in came_from]
        states = frontier

def get_neighbors(board):
    neighbors = list()
    x = board.index("0")
    if x >= 3:  # Up
        neighbors.append(board[0:x-3] + "0" + board[x-2:x] + board[x-3] + board[x+1:])
    if x % 3:  # Left
        neighbors.append(board[0:x-1] + "0" + board[x-1] + board[x+1:])
    if x % 3 < 2:  # Right
        neighbors.append(board[0:x] + board[x+1] + "0" + board[x+2:])
    if x < 6:  # Down
        neighbors.append(board[0:x] + board[x+3] + board[x+1:x+3] + "0" + board[x+4:])
    return neighbors

path = solve_squares(get_empty_board())
print("solution:")
for board in path:
    print_board(board)
    print()
,

展平棋盘并使用包含预映射动作的字典将大大简化和加速逻辑。建议使用 BFS 方法以获得最少的移动次数。为了跟踪访问过的位置,可以将展平的板存储为一个元组,这将允许直接使用一个集合来有效地跟踪和验证以前的状态:

# move mapping (based on position of the zero/empty block)
moves = { 0: [1,3],1: [0,2,4],2: [1,5],3: [0,4,6],4: [1,3,5,7],5: [2,8],6: [3,7: [4,6,8: [5,7] }

from collections import deque               
def solve(board,target=(1,7,8,0)):
    if isinstance(board[0],list):  # flatten board
        board  = tuple(p for r in board for p in r)
    if isinstance(target[0],list): # flatten target
        target = tuple(p for r in target for p in r)
    seen = set()
    stack = deque([(board,[])])         # BFS stack with board/path
    while stack:
        board,path = stack.popleft()    # consume stack breadth first
        z = board.index(0)              # position of empty block
        for m in moves[z]:              # possible moves
            played = list(board)                       
            played[z],played[m] = played[m],played[z] # execute move
            played = tuple(played)                    # board as tuple
            if played in seen: continue               # skip visited layouts
            if played == target: return path + [m]    # check target
            seen.add(played)                          
            stack.append((played,path+[m]))           # stack move result

输出:

initial = [ [7,2],[8,0],[1,6]
          ]
target  = [ [1,[7,5]
          ]

solution = solve(initial,target) # runs in 0.19 sec.

# solution = (flat) positions of block to move to the zero/empty spot
[4,1,4]


board = [p for r in initial for p in r]
print(*(board[i:i+3] for i in range(0,9,3)),sep="\n")
for m in solution:
    print(f"move {board[m]}:")
    z = board.index(0)
    board[z],board[m] = board[m],board[z]
    print(*(board[i:i+3] for i in range(0,sep="\n")

[7,2]
[8,0]
[1,6]
move 3:
[7,3]
[1,6]
move 4:
[7,6]
move 7:
[0,6]
move 8:
[8,2]
[0,6]
move 1:
[8,2]
[1,3]
[0,6]
move 5:
[8,3]
[5,6]
move 4:
[8,6]
move 7:
[8,6]
move 8:
[0,6]
move 1:
[1,6]
move 7:
[1,2]
[7,6]
move 8:
[1,6]
move 2:
[1,0]
[7,6]
move 3:
[1,3]
[7,0]
[5,6]
move 6:
[1,6]
[5,0]
move 4:
[1,4]
move 5:
[1,6]
[0,4]
move 7:
[1,6]
[7,4]
move 8:
[1,3]
[8,4]
move 6:
[1,4]
move 4:
[1,4]
[7,0]
move 5:
[1,5]
move 6:
[1,5]