【问题标题】:N-puzzle a-star python solver efficiencyN-puzzle a-star python求解器效率
【发布时间】:2018-02-27 07:10:18
【问题描述】:

我编写了一个用于解决滑动块/n 谜题的 a-star 算法。它在小谜题上运行良好,但随着复杂性的增加而陷入困境。

我已经实现了几种提高效率的方法(heapq 等),但我的想法已经走到了尽头。你能想到我还能做些什么来改进它吗?

我的代码在这里: https://repl.it/@Jaspirian/SimilarWoodenMemoryallocator

一些重要的部分:

启发式:

def heuristic_estimate_manhattan(self, other):
    """
    Finds the heuristic estimation of the cost to reach another state from this one.
    This heuristic is based on "manhattan distance."
    """
    estimate = 0

    for index in range(len(self.blocks)):
        estimate += abs(other.blocks[index][0] - self.blocks[index][0]) + abs(other.blocks[index][1] - self.blocks[index][1])

    return estimate

邻域函数:

def get_neighbors(self, previous):
    """
    Gets all adjacent neighbors of the state, minus the previous.
    This function gives 7 neighbors: 4 orthogonal, 4 diagonal, with the previous state trimmed.
    """
    neighbors = []

    moves = ((-1,0),(1,0),(0,-1),(0,1))
    zeroLoc = self.blocks[0]

    for move in moves:
        # swap 0 and whatever
        newBlocks = copy.deepcopy(self.blocks)
        newZeroLoc = (zeroLoc[0] + move[0], zeroLoc[1] + move[1])
        # skip this state if we've moved off the board
        if newZeroLoc[0] < 0 or newZeroLoc[1] < 0 or newZeroLoc[0] > self.width-1 or newZeroLoc[1] > self.height-1:
            # print("we've moved off the board.")
            continue
        # skip this state if it's the same as the previous
        if previous and previous.blocks[0] == newZeroLoc:
            # print("this is just the same!")
            continue

        # move the 0
        newBlocks[0] = newZeroLoc

        # move whatever's in that location...
        # to the previous one
        for face, location in newBlocks.items():
            if face != 0 and location == newZeroLoc:
                newBlocks[face] = zeroLoc

        neighbor = Block_Puzzle(newBlocks)
        neighbors.append(neighbor)

    return neighbors

a-star 算法:

def aStar(start, goal):
"""
A star search algorithm. Takes a start state and an end state.
While there are available moves, loops through them and exits if the end is found.
Returns the list of states that are the "quickest" way to the end.
"""
...
openHeap = [start]
heapq.heapify(openHeap)
...
# While there are yet nodes to inspect,
while(len(openHeap) > 0):
    # Pop the lowest f-score state off. 
    current = heapq.heappop(openHeap)

    # print(len(openHeap))

    # If we've reached the goal:
    if current == goal:
        # return the list of states it took to get there.
        ...
        return path

    # make sure we won't visit this state again.
    closedDict[current] = True

    # For each possible neighbor of our current state,
    for neighbor in current.get_neighbors(cameFrom.get(current)):
        # Skip it if it's already been evaluated
        if neighbor in closedDict:
            continue

        # Add it to our open heap
        heapq.heappush(openHeap, neighbor)

        tentative_gScore = gScore[current] + 1
        # If it takes more to get here than another path to this state, skip it.
        if tentative_gScore >= gScore[neighbor]:
            continue

        # If we got to this point, add it!
        cameFrom[neighbor] = current
        gScore[neighbor] = tentative_gScore
        fScore[neighbor] = gScore[neighbor] + neighbor.heuristic_estimate_manhattan(goal)

return None

【问题讨论】:

  • IDA* 在这个问题上比 A* 快。借助强大的启发式算法,您可以使用 IDA* 获得非常好的性能。
  • 为此我将自己限制在 A*,但我同意 IDA* 更胜一筹。

标签: python performance a-star


【解决方案1】:

get_neighbors 中,newBlocks 在这些检查期间不使用(如边界检查)。如果其中任何一项检查失败,deepcopy()(或 jsmolka 回答的常规copy())将是浪费时间。您可以将该副本移动到检查之后。


在算法本身中,我建议将启发式乘以一个略大于 1 的数字。例如:

fScore[neighbor] = gScore[neighbor] + 1.0001 * neighbor.heuristic_estimate_manhattan(goal)

这应该以这样一种方式自动实现平局,我们更喜欢成本主要为g(实际成本、可靠信息、已知正确)的路径,而不是总成本相同的路径f很大程度上取决于启发式h(启发式,猜测,可能不完全正确/可靠)。这通常是 A* 的最佳决胜局。从理论上讲,这种乘法可能会使您的启发式算法不可接受,但如果乘数与1.0 足够接近,那也没关系。


假设current 有一个分数f_current,并且刚刚从openHeap 中弹出。假设一个新生成的邻居最终得到完全相同的f 分数(只是现在更大的g 组件和更小的h 组件)。您肯定知道,在下一次迭代中,具有此分数的节点将立即再次弹出。这意味着实际上将它推入堆然后再次弹出是低效的。

同时提供一个单独的(未排序的)堆栈会更有效。如果f 分数等于父母的f 分数,则将节点推送到此堆栈而不是堆上。如果此堆栈非空,请始终从此而不是中弹出节点,而不是从您的堆中弹出。仅当此堆栈为空时才从堆中弹出。

注意:这个想法在结合上述基于乘法的平局时会变得复杂。如果您可以手动指定堆的排序标准,您还可以以不同的方式实现平局(例如,如果基于f 得分相等的节点,如果它具有更大的g / 更小@987654340,则显式将其视为更小@)。

【讨论】:

  • 非常感谢!移动副本对性能有很大的改进,启发式加权也是如此。我对未排序的、独立的 F 分数相等状态堆栈进行了试验,发现它的影响可以忽略不计。然而,最大的变化是在我的堆推中。有关详细信息,请参阅我的答案。
【解决方案2】:

如果我理解正确self.blocks 是一本字典。您可以使用复制它

newBlocks = self.blocks.copy()  # or: dict(self.blocks)

节省一些时间。 deepcopy 在这里不需要。

【讨论】:

  • 谢谢!这对性能有很大的改进。有什么其他的东西在你身上跳出来吗?
  • 不是真的,我最近尝试为我的迷宫求解器实现 A*,但我只是想出了一个性能糟糕的解决方案。也许您应该尝试不同的算法,例如深度优先搜索以获得更好的性能。
  • 我的理解是 A-star 是解决这类问题的最佳算法(除了 IDA*)。如果在接下来的半天里没有其他人提供其他建议,我会将您的建议标记为答案。
【解决方案3】:

Dennis 和 jsmolka 都帮助了我,但我的代码的致命缺陷就在这里:

# Add it to our open heap
    heapq.heappush(openHeap, neighbor)

    ...

    # If we got to this point, add it!
    cameFrom[neighbor] = current
    gScore[neighbor] = tentative_gScore
    fScore[neighbor] = gScore[neighbor] + neighbor.heuristic_estimate_manhattan(goal)

我的理解是对象的 lt() 函数在它被推送到堆时被调用。如果是这种情况,我会将我的状态推送到堆中——然后再更改值,它无法更改顺序。

我重新设计了这一部分,现在同样的谜题需要 5.3 秒,多亏了 jsmolka 和 Dennis,从之前的 250 秒缩短了 86 秒。

完成的代码为here,相关部分如下。

for neighbor in current.get_neighbors(cameFrom.get(current)):
        # Skip it if it's already been evaluated
        if neighbor in closedSet:
            continue

        tentative_gScore = gScore[current] + 1
        # If this path costs less than previous paths here...
        if tentative_gScore < gScore[neighbor]:
            # Update the values for this state.
            cameFrom[neighbor] = current
            gScore[neighbor] = tentative_gScore
            fScore[neighbor] = gScore[neighbor] + (1.0001 * neighbor.heuristic_estimate_manhattan(goal))

        # Finally, add it to our open heap
        heapq.heappush(openHeap, neighbor)

【讨论】:

    猜你喜欢
    • 2012-08-15
    • 2012-08-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多