Skip to content

Latest commit

 

History

History
206 lines (165 loc) · 7.56 KB

File metadata and controls

206 lines (165 loc) · 7.56 KB

Derivation

Abstraction

  • Find all the paths from top-left to bottom-right and its efforts, pick the minimum one.

  • The “effort” of a path is the maximum absolute difference between two adjacent nodes in that path.

Simplification

1x1, 1x2, 2x1, 2x2

A, B    1, 3
C, D    2, 7

dfs(A, 0)
  dfs(B, 2)
    dfs(D, 4) = 4 (min)
  dfs(C, 1)
    dfs(D, 5) = 5

Brute Force

  1. Run DFS or BFS starting from top-left, update the effort during traversal, stop until reaching the bottom-right, add the current effort to effort list.

  2. Then find the minimum from effort list.

fun bruteForce(heights: Array<IntArray>): Int {
    val m = heights.size
    val n = heights[0].size
    val allEfforts = mutableListOf<Int>()
    val visited = Array(m) { BooleanArray(n) }
    dfs(heights, 0, 0, visited, 0, allEfforts)
    return allEfforts.min()
}

private fun dfs(heights: Array<IntArray>, row: Int, col: Int, visited: Array<BooleanArray>, currentEffort: Int, allEfforts: MutableList<Int>) {
    val m = heights.size
    val n = heights[0].size
    if (row !in 0 until m || col !in 0 until n) return
    if (visited[row][col]) return
    if (row == m - 1 && col == n - 1) {
        allEfforts.add(currentEffort)
        return 
    }

    visited[row][col] = true
    for (d in directions) {
        val newRow = row + d[0]
        val newCol = col + d[1]
        if (newRow !in 0 until m || newCol !in 0 until n) continue
        if (visited[newRow][newCol]) continue

        val newEffort = abs(heights[row][col] - heights[newRow][newCol])
        dfs(heights, newRow, newCol, visited, maxOf(currentEffort, newEffort), allEfforts)
    }
    // Backtrack
    visited[row][col] = false
}
  • Time complexity: We are going to find all the possible paths so we need to backtrack at each position, and there are at most 3 adjacent nodes to visit next, the time complexity is 3^(m x n)

    • O(m x n), this is wrong as we are exploring the possible paths from a single node, the time complexity is O(m x n) for a single path, it's not the total complexity.

Bottleneck

Suppose we have exact two paths A and B, path A has all minimum efforts, path B has all maximum efforts, then exploring path B is a waste.

1a, 100
1b, 1c

1a -> 100 -> 1c: 99
1a -> 1b -> 1c: 0 (winner)

Pattern Recognition

  • What if we can always explore the minimum effort at each step, then see if we can reach the destination. If not, we explore the sub-optimal path after that.

    • We prioritize to visit the path with minimum effort at each step.

→ Priority Queue + Dijkstra’s

Binary Search

Min-Max 為何是二分?

  • 裡面的 Max: 在 1631 題中,我們看的是「Maximum Effort(路徑上的最大高度差)」。

    在物理上,你可以把這個 Maximum 想像成你的「腿長限制」。

    如果你選定了一條路徑,這條路徑的 Maximum Effort 是 K,意思就是:「只要你的腿長有 K,你就能無懼路徑上的任何起伏,順利走到終點。」

  • 外面的 Mi:「為了順利走到終點,我的腿長『至少』要多長?」。

我們就從 1, 2, 3, ... 開始往上找,看看這樣能不能走到終點?可以發現,答案呈現 X, X, X, O, O, O 的狀態,在超過某個數值之後就開始都可以通過 (單調性),這就是一個典型的二分搜尋問題了。

Binary search is for finding a value in "sorted array", but there is no such sorted array in this problem, either the grid or paths is unsorted. The "sorted array" is hidden in the range of possible answer.

  • Let's say the maximum absolute difference range from 0 to 100.
  • The answer must be in that range [0..100].
  • This range [0..100] is the sorted array that we can apply the binary search.

We use effort to represent the maximum absolute difference in heights between two consecutive cells of the route. (Aligned the problem description)

Let's take a look how do we apply binary search on answer in this problem:

  1. Search range: Given a minimum and maximum in the grid, the possible maximum effort = abs(max - min), the possible minimum effort is 0 (same height).
  2. Monotonicity: If there exists a path which effort is k, then we can definitely find a path which effort is k + 1 or larger k. (It's constraints)

We guess an effort k and check if there exists a path from source to destination that under that effort constraints. If we can reach the destination (via DFS or BFS), we try to find a smaller threshold. Otherwise, we try to find a larger threshold.

可以上下左右走,沒有無後效性,無法用動態規劃做。無腦搜索的話因為沒有什麼障礙或限制,效率非常差。可以二分猜答案;也可以貪心找 (Dijkstra's),先找出所有的落差值,從最小的值開始串,看能不能串出從左上到右下的路徑。

fun minimumEffortPath(heights: Array<IntArray>): Int {
    val m = heights.size
    val n = heights[0].size
    var left = 0 // Minimum effort is 0, not 1, the height difference can be 0. (same height)
    var right = abs(heights.maxOf { it.max() } - heights.minOf { it.min() })
    while (left <= right) {
        val middle = left + (right - left) / 2
        val canReach = dfs(heights, 0, 0, Array(m) { BooleanArray(n) }, middle)
        if (canReach) {
            right = middle - 1
        } else {
            left = middle + 1
        }
    }
    return left
}

// We traverse the grid using DFS and check if we can reach the destination with the threshold.
private fun dfs(
    heights: Array<IntArray>,
    x: Int,
    y: Int,
    visited: Array<BooleanArray>,
    threshold: Int
): Boolean {
    val m = heights.size
    val n = heights[0].size
    if (x == m - 1 && y == n - 1) return true
    visited[x][y] = true

    var result = false
    for (d in directions) {
        val newX = x + d[0]
        val newY = y + d[1]
        if (newX !in 0 until m || newY !in 0 until n) continue
        if (visited[newX][newY] == true) continue
        val diff = abs(heights[x][y] - heights[newX][newY])
        if (diff <= threshold) {
            result = dfs(heights, newX, newY, visited, threshold) || result
        }
    }
    return result
}
  • Time Complexity: O(m * n * log(R)) where R is the maximum possible value range.
  • Space Complexity: O(m * n).

Dijkstra

Explore the path greedily, always choose the path with the minimum effort.

fun dijkstra(heights: Array<IntArray>): Int {
    val m = heights.size
    val n = heights[0].size
    val efforts = Array(m) { IntArray(n) { Int.MAX_VALUE } }
    val queue = PriorityQueue(compareBy<Cell> { it.effort })
    queue.add(Cell(0, 0, 0))
    efforts[0][0] = 0
    while (queue.isNotEmpty()) {
        val (row, col, effort) = queue.poll()

        // Skip the stale
        if (effort > efforts[row][col]) continue
        // Reach the destination
        if (row == m - 1 && col == n - 1) {
            return effort
        }

        for (d in directions) {
            val newRow = row + d[0]
            val newCol = col + d[1]
            if (newRow !in 0 until m || newCol !in 0 until n) continue

            val nextEffort = abs(heights[newRow][newCol] - heights[row][col])
            // Update the effort to accumulate the adjacent cell.
            val newEffort = maxOf(effort, nextEffort)
            if (newEffort < efforts[newRow][newCol]) {
                queue.add(Cell(newRow, newCol, newEffort))
                efforts[newRow][newCol] = newEffort
            }
        }
    }
    return -1
}