weekly contest 450。
BFS + 傳送門,總記得有做過相似題,一時找不到。

題目

https://leetcode.com/problems/grid-teleportation-traversal/description/

解法

只能走 ‘.’,不能走 ‘#’。
單純不管字母就是普通的 BFS 求最短路。


字母之間可以成本 0 移動,且相同字母可能出現多次。
每次可移動方向相當於 4 個相鄰格子,還有相同子母的格子。

題目還限制每個傳送字母至多使用一次
假設有三個 A,記做 A1, A2, A3。
從 A1 跳到 A2 再跳到 A3 是不合法的。
但是也沒必要,真要跳的話直接 A1 到 A3 就好,經過其他中繼點沒意義。

預處理所有相同字母的格子,之後用於求最短路。
在最差情況下,所有格子的字母都相同、可互相傳送,每格都枚舉傳送位置複雜度高達 O((MN)^2)。
根據上述結論,傳送時經過其他點沒意義,也就是說同樣字母跳第二次也沒意義
對於每個字母,只有第一次碰到時需要處理。


普通移動成本 1,傳送成本 0。
有不同的移動成本,需要優先處理成本更低的路徑,故改用 dijkstra。

時間複雜度 O(MN log MN)。
空間複雜度 O(MN)。

class Solution:
    def minMoves(self, matrix: List[str]) -> int:
        M, N = len(matrix), len(matrix[0])

        # build teleport
        jump = defaultdict(list)
        for r in range(M):
            for c in range(N):
                x = matrix[r][c]
                if x in ".#":
                    continue
                jump[x].append((r, c))

        dist = [[inf] * N for _ in range(M)]
        dist[0][0] = 0
        h = [(0, 0, 0)]  # cost, r, c
        while h:
            cost, r, c = heappop(h)
            if r == M-1 and c == N-1:
                return cost
            if cost > dist[r][c]:
                continue

            # jump
            x = matrix[r][c]
            if x in jump:
                for (rr, cc) in jump[x]:
                    if cost < dist[rr][cc]:
                        dist[rr][cc] = cost
                        heappush(h, (cost, rr, cc))
                del jump[x]  # can only jump once

            # move
            for dx, dy in pairwise([0, 1, 0, -1, 0]):
                rr, cc = r+dx, c+dy
                if rr < 0 or rr == M or cc < 0 or cc == N or matrix[rr][cc] == "#":
                    continue
                new_cost = cost + 1
                if new_cost < dist[rr][cc]:
                    dist[rr][cc] = new_cost  # important pruning
                    heappush(h, (new_cost, rr, cc))

        return -1

注意到路徑成本只有 0 和 1 兩種,可用 0-1 BFS 優化。
使用雙端隊列,傳送成本為 0,優先處理,加到隊列前方;普通移動成本 1,晚點處理,加到後方。

時間複雜度 O(MN)。
空間複雜度 O(MN)。

class Solution:
    def minMoves(self, matrix: List[str]) -> int:
        M, N = len(matrix), len(matrix[0])

        # build teleport
        jump = defaultdict(list)
        for r in range(M):
            for c in range(N):
                x = matrix[r][c]
                if x in ".#":
                    continue
                jump[x].append((r, c))

        dist = [[inf] * N for _ in range(M)]
        dist[0][0] = 0
        q = deque([(0, 0, 0)])  # cost, r, c
        while q:
            cost, r, c = q.popleft()
            if r == M-1 and c == N-1:
                return cost
            if cost > dist[r][c]:
                continue

            # jump
            x = matrix[r][c]
            if x in jump:
                for (rr, cc) in jump[x]:
                    if cost < dist[rr][cc]:
                        dist[rr][cc] = cost
                        q.appendleft((cost, rr, cc))
                del jump[x]  # can only jump once

            # move
            for dx, dy in pairwise([0, 1, 0, -1, 0]):
                rr, cc = r+dx, c+dy
                if rr < 0 or rr == M or cc < 0 or cc == N or matrix[rr][cc] == "#":
                    continue
                new_cost = cost + 1
                if new_cost < dist[rr][cc]:
                    dist[rr][cc] = new_cost  # important pruning
                    q.append((new_cost, rr, cc))

        return -1