【问题标题】:What's the problem in my BFS implementation for 8 squares我的 8 个方格的 BFS 实现有什么问题
【发布时间】:2021-02-26 04:00:11
【问题描述】:

我设计了一种算法,可以使用 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],
    [8,0,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()

【问题讨论】:

  • 您收到了两个答案。你能留下一些反馈吗?

标签: python algorithm data-structures


【解决方案1】:

问题确实是性能。要了解速度,请将以下 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()

【讨论】:

  • 谢谢你的解释很好。再次感谢!抱歉这么晚才回复
【解决方案2】:

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

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

我们可以通过下面的例子来形象化第二次优化。假设您将 &lt;state, parent-state&gt; 映射为键值:

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

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

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

【讨论】:

    【解决方案3】:

    展平棋盘并使用包含预映射动作的字典将大大简化和加速逻辑。建议使用 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,4,8],
              6: [3,7],
              7: [4,6,8],
              8: [5,7] }
    
    from collections import deque               
    def solve(board,target=(1,2,3,4,5,6,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,4,2],
                [8,3,0],
                [1,5,6]
              ]
    target  = [ [1,2,3],
                [8,0,4],
                [7,6,5]
              ]
    
    solution = solve(initial,target) # runs in 0.19 sec.
    
    # solution = (flat) positions of block to move to the zero/empty spot
    [4, 1, 0, 3, 6, 7, 4, 1, 0, 3, 4, 1, 2, 5, 8, 7, 6, 3, 4, 5, 8, 7, 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,9,3)),sep="\n")
    
    [7, 4, 2]
    [8, 3, 0]
    [1, 5, 6]
    move 3:
    [7, 4, 2]
    [8, 0, 3]
    [1, 5, 6]
    move 4:
    [7, 0, 2]
    [8, 4, 3]
    [1, 5, 6]
    move 7:
    [0, 7, 2]
    [8, 4, 3]
    [1, 5, 6]
    move 8:
    [8, 7, 2]
    [0, 4, 3]
    [1, 5, 6]
    move 1:
    [8, 7, 2]
    [1, 4, 3]
    [0, 5, 6]
    move 5:
    [8, 7, 2]
    [1, 4, 3]
    [5, 0, 6]
    move 4:
    [8, 7, 2]
    [1, 0, 3]
    [5, 4, 6]
    move 7:
    [8, 0, 2]
    [1, 7, 3]
    [5, 4, 6]
    move 8:
    [0, 8, 2]
    [1, 7, 3]
    [5, 4, 6]
    move 1:
    [1, 8, 2]
    [0, 7, 3]
    [5, 4, 6]
    move 7:
    [1, 8, 2]
    [7, 0, 3]
    [5, 4, 6]
    move 8:
    [1, 0, 2]
    [7, 8, 3]
    [5, 4, 6]
    move 2:
    [1, 2, 0]
    [7, 8, 3]
    [5, 4, 6]
    move 3:
    [1, 2, 3]
    [7, 8, 0]
    [5, 4, 6]
    move 6:
    [1, 2, 3]
    [7, 8, 6]
    [5, 4, 0]
    move 4:
    [1, 2, 3]
    [7, 8, 6]
    [5, 0, 4]
    move 5:
    [1, 2, 3]
    [7, 8, 6]
    [0, 5, 4]
    move 7:
    [1, 2, 3]
    [0, 8, 6]
    [7, 5, 4]
    move 8:
    [1, 2, 3]
    [8, 0, 6]
    [7, 5, 4]
    move 6:
    [1, 2, 3]
    [8, 6, 0]
    [7, 5, 4]
    move 4:
    [1, 2, 3]
    [8, 6, 4]
    [7, 5, 0]
    move 5:
    [1, 2, 3]
    [8, 6, 4]
    [7, 0, 5]
    move 6:
    [1, 2, 3]
    [8, 0, 4]
    [7, 6, 5]
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-04-18
      • 2019-06-07
      • 2021-07-25
      • 2011-09-08
      • 2016-05-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多