LeetCode的“迷宫II”问题:为什么此解决方案超出了时间限制?

时间:2019-08-17 23:09:13

标签: python algorithm heap shortest-path dijkstra

我正在尝试解决LeetCode的The Maze II问题,

enter image description here enter image description here enter image description here

但是我遇到了“超过时间限制”失败的测试:

enter image description here

我的方法是将Dijkstra的算法与最低优先级队列一起使用,类似于此处描述的https://leetcode.com/problems/the-maze-ii/discuss/351244/Python3-heapq-priority-queue-beats-100,因此我有点困惑为什么我的解决方案超时了。

这是我尝试的解决方案:

import collections
import heapq
from typing import List, Tuple, Callable, Optional, Dict, Set


class Solution:
    def shortestDistance(self, maze: List[List[int]], start: List[int], destination: List[int]) -> int:
        return shortest_distance(
            maze=maze, start=tuple(start), destination=tuple(destination))


def shortest_distance(maze: List[List[int]], start: Tuple[int, int], destination: Tuple[int, int]) -> int:
    distances: Dict[Tuple[int, int], int] = collections.defaultdict(lambda: float('inf'))
    distances[start] = 0

    heap = [(0, start)]
    visited: Set[Tuple[int, int]] = {start}

    while heap:
        distance, coord = heapq.heappop(heap)
        visited.add(coord)

        if coord == destination:
            return distance

        for neighbor, d in get_neighbors(coord, maze):
            distances[neighbor] = min(distances[neighbor], distances[coord] + d)
            if neighbor not in visited:
                heapq.heappush(heap, (distances[neighbor], neighbor))

    return -1


DIRECTIONS: List[Callable[[Tuple[int, int]], Tuple[int, int]]] = [
    lambda coord: (coord[0] - 1, coord[1]),     # up
    lambda coord: (coord[0] + 1, coord[1]),     # down
    lambda coord: (coord[0], coord[1] - 1),     # left
    lambda coord: (coord[0], coord[1] + 1),     # right
]


def get_neighbors(coord: Tuple[int, int], maze: List[List[int]]) -> List[Tuple[Tuple[int, int], int]]:
    return [tup for tup in [
        get_neighbor(coord, maze, direction) for direction in DIRECTIONS]
        if tup[0] is not None]


def get_neighbor(
        coord: Tuple[int, int],
        maze: List[List[int]],
        direction: Callable[[Tuple[int, int]], Tuple[int, int]]) -> Tuple[Optional[Tuple[int, int]], int]:
    dist = -1
    prev, curr = None, coord
    while valid(curr, maze):
        prev, curr = curr, direction(curr)
        dist += 1
    return (prev, dist) if prev != coord else (None, -1)


def valid(coord: Tuple[int, int], maze: List[List[int]]) -> bool:
    return in_bounds(coord, maze) and maze[coord[0]][coord[1]] == 0


def in_bounds(coord: Tuple[int, int], maze: List[List[int]]) -> bool:
    return 0 <= coord[0] < len(maze) and 0 <= coord[1] < len(maze[0])

据我了解,将节点推送到堆上的时间复杂度为O(log N),并且由于此情况对于图中的每个节点都发生一次,因此我希望总时间复杂度为{{1} },这似乎应该是一个“高效”的解决方案。

此算法是否存在我忽略的效率低下的问题?

1 个答案:

答案 0 :(得分:1)

您对每个坐标元组仅被推入队列一次的假设可能不正确。如果您在访问同一位置之前从两个不同的相邻位置进入同一位置,则可能会按两次该位置。

不良的ASCII艺术图:

A B
B C D

如果从A位置开始,则在处理B时会将其两个邻居,即两个A位置添加到队列中。然后,您将在处理B个节点的共同邻居C之前同时处理这两个节点。由于每个B位置都将其邻居添加到队列中,因此C将被添加两次。该重复将继续,每次处理C时,都将其邻居D添加到堆中。

Dijkstra算法的通用版本无法轻松避免将位置多次放入队列中(因为到节点的新路径可能比您推送但尚未探索的路径短,而且没有简单的方法查找和修改堆中的值)。但是,您可以防止重复出现一次。只是拒绝处理已经访问过的任何职位:

visited: Set[Tuple[int, int]] = set()        # this set needs to start empty now

while heap:
    distance, coord = heapq.heappop(heap)
    if coord in visited:                     # skip repeat visits
        continue
    visited.add(coord)

我还要指出,您的代码可能还有另一个问题(与性能无关)。它们以您的方式生成邻居,只有当当前路径死胡同时,您才会改变方向。例如,我认为您无法解决这个迷宫(您尝试从S转到E,其中.是迷宫的开放空间:

S . . 
  E

您的邻居代码将告诉您S的唯一邻居是最右边的点,而该位置的唯一邻居将是S(您已经访问过)。您永远都不会停在第一行的中间,这样您就可以更改方向以向下到达出口。