1631. Path With Minimum Effort

Path With Minimum Effort - LeetCode

You are a hiker preparing for an upcoming hike. You are given heights, a 2D array of size rows x columns, where heights[row][col] represents the height of cell (row, col). You are situated in the top-left cell, (0, 0), and you hope to travel to the bottom-right cell, (rows-1, columns-1) (i.e., 0-indexed). You can move up, down, left, or right, and you wish to find a route that requires the minimum effort. A route’s effort is the maximum absolute difference in heights between two consecutive cells of the route. Return the minimum effort required to travel from the top-left cell to the bottom-right cell.

Example 1: Input: heights = [[1,2,2],[3,8,2],[5,3,5]] Output: 2 Explanation: The route of [1,3,5,3,5] has a maximum absolute difference of 2 in consecutive cells. This is better than the route of [1,2,2,2,5], where the maximum absolute difference is 3. Example 2: Input: heights = [[1,2,3],[3,8,4],[5,3,5]] Output: 1 Explanation: The route of [1,2,3,4,5] has a maximum absolute difference of 1 in consecutive cells, which is better than route [1,3,5,3,5]. Example 3: Input: heights = [[1,2,1,1,1],[1,2,1,2,1],[1,2,1,2,1],[1,2,1,2,1],[1,1,1,2,1]] Output: 0 Explanation: This route does not require any effort.

Constraints:

rows == heights.length
columns == heights[i].length
1 <= rows, columns <= 100
1 <= heights[i][j] <= 106

  • code TLE DP usually only works with directional matrix, one way or two, for this problem 4 directions are valid.
class Solution:
    def minimumEffortPath(self, heights: List[List[int]]) -> int:
        direction = [[-1, 0], [1, 0], [0, 1], [0, -1]]
        m = len(heights)
        n = len(heights[0])
        
        dis = [[inf for _ in range(n)] for _ in range(m)]
        dis[0][0] = 0
        pre = copy.deepcopy(dis)
        for round in range(m * n):
            for i in range(m):
                for j in range(n):
                    for dx, dy in direction:
                        nei_x, nei_y = i + dx, j + dy
                        if 0 <= nei_x < m and 0 <= nei_y < n:
                            dis[i][j] = min(dis[i][j], max(abs(heights[i][j] - heights[nei_x][nei_y]), dis[nei_x][nei_y]))
            if pre == dis: break
            pre = copy.deepcopy(dis)
                    
        return dis[-1][-1]
        
  • code TLE Dijkstra, the missing point: before pushing neighbour nodes, check if the new diff[nx][ny] is smaller,
class Solution:
    def minimumEffortPath(self, heights: List[List[int]]) -> int:
        m, n = len(heights), len(heights[0])
        
        diff = [[inf for _ in range(n)] for _ in range(m)]
        diff[0][0] = 0

        q = [[0, (0,0)]] # max diff, point
        direction = [[-1,0],[1,0],[0,1],[0,-1]]
        visited = set()
        while q:
            cur_diff, cur = heapq.heappop(q)
            if cur_diff >= diff[-1][-1]: break
            visited.add(cur)
            x, y = cur
            for dx, dy in direction:
                nx, ny = x + dx, y + dy
                if 0 <= nx < m and 0 <= ny < n and (nx, ny) not in visited:
                    diff[nx][ny] = min(diff[nx][ny], max(abs(heights[nx][ny] - heights[x][y]), cur_diff))
                    heapq.heappush(q, [diff[nx][ny], (nx, ny)])
                                  
        return diff[-1][-1]
  • code #Dijkstra as long as a new edge won’t decrease the path, i.e. diff won’t decrease, we can potentially use Dijkstra(no negative edges)
class Solution:
    def minimumEffortPath(self, heights: List[List[int]]) -> int:
        m, n = len(heights), len(heights[0])
        
        diff = [[inf] * n for _ in range(m)]
        diff[0][0] = 0

        q = [[0, 0, 0]] # max diff, (x, y)
        direction = [[-1,0],[1,0],[0,1],[0,-1]]
        visited = [[False] * n for _ in range(m)]
        while q:
            cur_diff, x, y = heapq.heappop(q)
            visited[x][y] = True
            for dx, dy in direction:
                nx, ny = x + dx, y + dy
                if 0 <= nx < m and 0 <= ny < n and not visited[nx][ny]:
                    newdiff = min(diff[nx][ny], max(abs(heights[nx][ny] - heights[x][y]), cur_diff))
                    if newdiff < diff[nx][ny]:
                        diff[nx][ny] = newdiff
                        heapq.heappush(q, [diff[nx][ny], nx, ny])
                                  
        return diff[-1][-1]
  • code #unionfind sort all edges and start to merge, as long as source and end has the same root.
class Solution:
    def minimumEffortPath(self, heights: List[List[int]]) -> int:
        class UnionFind:
            def __init__(self, size):
                self.parent = [x for x in range(size)]
                self.rank = [0]*(size)

            def find(self, i):
                if self.parent[i] != i:
                    self.parent[i] = self.find(self.parent[i])
                return self.parent[i]

            def union(self, x, y):
                parent_x = self.find(x)
                parent_y = self.find(y)
                if parent_x != parent_y:
                    if self.rank[parent_x] > self.rank[parent_y]:
                        self.parent[parent_y] = parent_x
                    elif self.rank[parent_x] < self.rank[parent_y]:
                        self.parent[parent_x] = parent_y
                    else:
                        self.parent[parent_y] = parent_x
                        self.rank[parent_x] += 1

        row = len(heights)
        col = len(heights[0])
        if row == 1 and col == 1:
            return 0

        edge_list = []
        for current_row in range(row):
            for current_col in range(col):
                if current_row > 0:
                    difference = abs(
                        heights[current_row][current_col] -
                        heights[current_row - 1][current_col])
                    edge_list.append(
                        (difference, current_row * col + current_col,
                         (current_row - 1) * col + current_col))
                if current_col > 0:
                    difference = abs(
                        heights[current_row][current_col] -
                        heights[current_row][current_col - 1])
                    edge_list.append(
                        (difference, current_row * col + current_col, current_row
                         * col + current_col - 1))
        edge_list.sort()
        union_find = UnionFind(row*col)

        for difference, x, y in edge_list:
            union_find.union(x, y)
            if union_find.find(0) == union_find.find(row*col-1):
                return difference
        return -1
  • code BFS + binary search
class Solution:
    def minimumEffortPath(self, heights: List[List[int]]) -> int:
        row = len(heights)
        col = len(heights[0])

        def canReachDestinaton(x, y, mid):
            if x == row-1 and y == col-1:
                return True
            visited[x][y] = True
            for dx, dy in [[0, 1], [1, 0], [0, -1], [-1, 0]]:
                adjacent_x = x + dx
                adjacent_y = y + dy
                if 0 <= adjacent_x < row and 0 <= adjacent_y < col and not visited[
                        adjacent_x][adjacent_y]:
                    current_difference = abs(
                        heights[adjacent_x][adjacent_y]-heights[x][y])
                    if current_difference <= mid:
                        visited[adjacent_x][adjacent_y] = True
                        if canReachDestinaton(adjacent_x, adjacent_y, mid):
                            return True
        left = 0
        right = 10000000
        while left < right:
            mid = (left + right)//2
            visited = [[False]*col for _ in range(row)]
            if canReachDestinaton(0, 0, mid):
                right = mid
            else:
                left = mid + 1
        return left

The real reason why the binary search approach works with BFS or DFS is that by defining a diff limit, the search space dramatically decreased, because we don’t need BFS/DFS to exhaust all the paths to the target anymore, we just need to find ONE path that satisfy the diff limit. Thus, if we find ourself at a Cell that satisfies a diff limit, we don’t care about other paths that get to this Cell anymore, that’s why we can use “visited” to eliminate paths exploration. Without a diff limit, we can no longer use this naive “visited” way of tracking and the search space becomes 3^(M*N) as we’d have to exhaust all the paths to find the one with min diff. -billmaxwell on leetcode https://leetcode.com/problems/path-with-minimum-effort/solution/977158