图的基础算法5 - Dijkstra

612 阅读3分钟

「迷宫」系列

490. 迷宫(Medium)

image.png

image.png

Solu:BFS / DFS

  • 只遍历每一次move的终点
  • mark '2':虽然是一次move的终点,但是已经遍历过了,不走回头路
    • PS:不能将visited的位置mark成'1',否则会改变grid的原有“地形”

Code 1:BFS

class Solution:
    def hasPath(self, maze: List[List[int]], start: List[int], destination: List[int]) -> bool:
        def bfs(i, j):
            queue = [(i, j)]
            maze[i][j] = 2  # mark visited
            while queue:
                i, j = queue.pop(0)
                for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
                    x, y = i, j
                    while 0 <= x + dx < m and 0 <= y + dy < n and maze[x + dx][y + dy] != 1:
                        x += dx
                        y += dy
                    if [x, y] == destination:
                        return True
                    if maze[x][y] != 2:
                        queue.append((x, y))
                        maze[x][y] = 2
            return False
        
        m, n = len(maze), len(maze[0])
        return bfs(start[0], start[1])

Code 2:DFS

class Solution:
    def hasPath(self, maze: List[List[int]], start: List[int], destination: List[int]) -> bool:
        def dfs(i, j):
            if i == destination[0] and j == destination[1]:
                return True
            maze[i][j] = 2  # mark visited
            for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
                x, y = i, j
                while 0 <= x + dx < len(maze) and 0 <= y + dy < len(maze[0]) and maze[x + dx][y + dy] != 1:
                    x += dx
                    y += dy
                if maze[x][y] == 0 and dfs(x, y):
                    return True
            return False
        
        return dfs(start[0], start[1])


505. 迷宫 II(Medium)

image.png

image.png

Solu:Dijkstra

  • 只对每一次move的终点去更新dp table

Code:

class Solution:
    def shortestDistance(self, maze: List[List[int]], start: List[int], destination: List[int]) -> int:
        def dijkstra():
            min_heap = []
            heapq.heappush(min_heap, (0, start[0], start[1]))
            while min_heap:
                cur_dist, i, j = heapq.heappop(min_heap)
                if i == destination[0] and j == destination[1]:  # found destination, 提前结束
                    return cur_dist
                if dist[i][j] < cur_dist:  # source->[i,j]的shortest path已经被更新过了
                    continue
                for di, dj in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
                    ni, nj = i, j
                    while 0 <= ni + di < m and 0 <= nj + dj < n and maze[ni + di][nj + dj] == 0:
                        ni += di
                        nj += dj
                    step = abs(ni - i) + abs(nj - j)
                    if step + dist[i][j] < dist[ni][nj]:
                        dist[ni][nj] = step + dist[i][j]
                        heapq.heappush(min_heap, (dist[ni][nj], ni, nj))
        
        m, n = len(maze), len(maze[0])
        dist = [[float('inf')] * n for _ in range(m)]
        dist[start[0]][start[1]] = 0
        dijkstra()
        return dist[destination[0]][destination[1]] if dist[destination[0]][destination[1]] < float('inf') else -1


499. 迷宫 III(Hard)

image.png

image.png

Solu:Dijkstra

  • 单源最短路径用dijkstra
  • dp table的修改规则:找到更短的一条path OR 这条path在相同的长度下拥有更小的字典序

Code:

class Solution:
    def findShortestWay(self, maze: List[List[int]], ball: List[int], hole: List[int]) -> str:
        def dijkstra():
            min_heap = []
            heapq.heappush(min_heap, (0, [ball[0], ball[1]], ''))
            while min_heap:
                cur_dist, cur_pos, cur_path = heapq.heappop(min_heap)
                i, j = cur_pos[0], cur_pos[1]
                if cur_pos == hole:
                    return
                if cur_dist > dist[i][j]:
                    continue
                for dir in dirs:
                    di, dj = dirs[dir][0], dirs[dir][1]
                    ni, nj = i, j
                    while 0 <= ni + di < m and 0 <= nj + dj < n and maze[ni + di][nj + dj] == 0:
                        ni += di
                        nj += dj
                        if [ni, nj] == hole:
                            break
                    step = abs(ni - i) + abs(nj - j)
                    if (step + cur_dist < dist[ni][nj]) or (
                            step + cur_dist == dist[ni][nj] and cur_path + dir < paths[ni][nj]):
                        dist[ni][nj] = step + cur_dist
                        paths[ni][nj] = cur_path + dir
                        heapq.heappush(min_heap, (step + cur_dist, [ni, nj], cur_path + dir))
        
        m, n = len(maze), len(maze[0])
        dirs = {'u': (-1, 0), 'd': (1, 0), 'l': (0, -1), 'r': (0, 1)}
        dist = [[float('inf')] * n for _ in range(m)]
        paths = [['impossible'] * n for _ in range(m)]
        dist[ball[0]][ball[1]] = 0
        paths[ball[0]][ball[1]] = ''
        dijkstra()
        return paths[hole[0]][hole[1]]


特殊的「最短路径」

407. 接雨水 II(Hard)

image.png

Solu:Dijkstra + 优先队列

  • 最外层的一圈(边界)是不会接到任何雨水的(会从边界流出)
  • 「路径高度」:从点(x, y)到边界的路径中出现的最大高度,必定有路径高度h ≥ heightMap[x][y] => Dijkstra
  • 问题的本质:从点(x, y)到边界的所有「路径高度」的最小值为多少
    • 根据「木桶理论」,点(x, y)的最终高度 = min{(x, y)四个邻点的最终高度}
  • *PS:可使用visited优化:因为Dijkstra中本身使用的最小堆,可以确保「出队元素」是「待更新的元素」的具有最小高度的邻点,并且会把「待更新元素」会被直接更新为“最终高度”

Code 1: Dijkstra + visited优化 ❤️

class Solution:
    def trapRainWater(self, heightMap: List[List[int]]) -> int:
        m, n = len(heightMap), len(heightMap[0])
        
        def dijkstra():
            pq = []
            visited = [[False] * n for _ in range(m)]
            # 边界不回储水,直接入queue
            for i in range(m):
                heapq.heappush(pq, (heightMap[i][0], i, 0))
                heapq.heappush(pq, (heightMap[i][n - 1], i, n - 1))
            for j in range(1, n - 1):
                heapq.heappush(pq, (heightMap[0][j], 0, j))
                heapq.heappush(pq, (heightMap[m - 1][j], m - 1, j))
            # 开始dijkstra
            ans = 0
            while pq:
                h, i, j = heapq.heappop(pq)
                for x, y in [(i - 1, j), (i + 1, j), (i, j - 1), (i, j + 1)]:
                    if 0 < x < m - 1 and 0 < y < n - 1 and not visited[x][y]:
                        visited[x][y] = True
                        ans += max(0, h - heightMap[x][y])
                        heapq.heappush(pq, (max(h, heightMap[x][y]), x, y))
            return ans
        
        return dijkstra()

Code 2:传统Dijkstra,无visited

class Solution:
    def trapRainWater(self, heightMap: List[List[int]]) -> int:
        m, n = len(heightMap), len(heightMap[0])
        
        def dijkstra():
            pq = []
            height = [[float('inf')] * n for _ in range(m)]
            # 边界不回储水,直接入queue
            for i in range(m):
                heapq.heappush(pq, (heightMap[i][0], i, 0))
                heapq.heappush(pq, (heightMap[i][n - 1], i, n - 1))
                height[i][0] = heightMap[i][0]
                height[i][n - 1] = heightMap[i][n - 1]
            for j in range(1, n - 1):
                heapq.heappush(pq, (heightMap[0][j], 0, j))
                heapq.heappush(pq, (heightMap[m - 1][j], m - 1, j))
                height[0][j] = heightMap[0][j]
                height[m - 1][j] = heightMap[m - 1][j]
            # 开始dijkstra
            while pq:
                cur_height, i, j = heapq.heappop(pq)
                if cur_height > height[i][j]:
                    continue
                for x, y in [(i - 1, j), (i + 1, j), (i, j - 1), (i, j + 1)]:
                    if 0 < x < m - 1 and 0 < y < n - 1:
                        new_height = max(cur_height, heightMap[x][y])
                        if new_height < height[x][y]:
                            height[x][y] = new_height
                            heapq.heappush(pq, (new_height, x, y))
            return height
        
        height = dijkstra()
        return sum(height[i][j] - heightMap[i][j] for i in range(m) for j in range(n))


1631. 最小体力消耗路径(Medium)

image.png

Solu:Dijkstra

重要结论:当题目允许往任意方向移动时,考察的往往就不是 DP 了!!而是图论!!

  • 把每个格子当成一个node,两个相邻node之间edge的权重 = 两个相邻格子之间的高度差绝对值
    • 题目转化为:需要找到一条从左上角到右下角的「最短路径」,其中len(path) = max{edge's weight in the path} => Dijkstra

Code:

class Solution:
    def minimumEffortPath(self, heights: List[List[int]]) -> int:
        m, n = len(heights), len(heights[0])
        
        def dijkstra() -> int:
            efforts = [[float('inf')] * n for _ in range(m)]
            efforts[0][0] = 0
            pq = []
            heapq.heappush(pq, (0, 0, 0))
            while pq:
                cur_effort, i, j = heapq.heappop(pq)
                if [i, j] == [m - 1, n - 1]:
                    return cur_effort
                if cur_effort > efforts[i][j]:
                    continue
                for x, y in [(i - 1, j), (i + 1, j), (i, j - 1), (i, j + 1)]:
                    if 0 <= x < m and 0 <= y < n:
                        new_effort = max(cur_effort, abs(heights[i][j] - heights[x][y]))
                        if new_effort < efforts[x][y]:
                            efforts[x][y] = new_effort
                            heapq.heappush(pq, (new_effort, x, y))
            return -1
        
        return dijkstra()