LC每日一题|20240506 - 741. 摘樱桃

71 阅读4分钟

LC每日一题|20240506 - 741. 摘樱桃

给你一个 n x n 的网格 grid ,代表一块樱桃地,每个格子由以下三种数字的一种来表示:

  • 0 表示这个格子是空的,所以你可以穿过它。
  • 1 表示这个格子里装着一个樱桃,你可以摘到樱桃然后穿过它。
  • -1 表示这个格子里有荆棘,挡着你的路。

请你统计并返回:在遵守下列规则的情况下,能摘到的最多樱桃数:

  • 从位置 (0, 0) 出发,最后到达 (n - 1, n - 1) ,只能向下或向右走,并且只能穿越有效的格子(即只可以穿过值为 0 或者 1 的格子);
  • 当到达 (n - 1, n - 1) 后,你要继续走,直到返回到 (0, 0) ,只能向上或向左走,并且只能穿越有效的格子;
  • 当你经过一个格子且这个格子包含一个樱桃时,你将摘到樱桃并且这个格子会变成空的(值变为 0 );
  • 如果在 (0, 0)(n - 1, n - 1) 之间不存在一条可经过的路径,则无法摘到任何一个樱桃。

提示:

  • n == grid.length
  • n == grid[i].length
  • 1 <= n <= 50
  • grid[i][j] 为 -10 或 1
  • grid[0][0] != -1
  • grid[n - 1][n - 1] != -1

题目等级:Hard

解题思路

确实是很难的一道题,抄题解了~

最开始太想当然了,想到的方案是从(0, 0)BFS到(n - 1, n - 1),然后DFS标记出去程的最大效益路径,再BFS回来。

以下是错误代码

class Solution {
    fun cherryPickup(grid: Array<IntArray>): Int {
        val c = Array<IntArray>(grid.size) { IntArray(grid[0].size) }
        if (grid[0][0] == 1) c[0][0] = 1
        val queue = ArrayDeque<IntArray>()
        queue.addLast(intArrayOf(0, 0))
        while (queue.isNotEmpty()) {
            repeat(queue.size) {
                val node = queue.removeFirst()
                val i = node[0]
                val j = node[1]
                if (i < grid.size - 1 && grid[i + 1][j] != -1) {
                    queue.addLast(intArrayOf(i + 1, j))
                    c[i + 1][j] = Math.max(c[i + 1][j], c[i][j] + if (grid[i + 1][j] == 0) 0 else 1)
                }
                if (j < grid[0].size - 1 && grid[i][j + 1] != -1) {
                    queue.addLast(intArrayOf(i, j + 1))
                    c[i][j + 1] = Math.max(c[i][j + 1], c[i][j] + if (grid[i][j + 1] == 0) 0 else 1)
                }
            }
        }

        if (c.last().last() == 0) return 0

        val first = c.last().last()
        
        grid[grid.size - 1][grid.size - 1] = 0
        fun dfs(i: Int, j: Int, cur: Int) {
            if (i == 0 && j == 0) return
            if (i > 0 && c[i][j] - c[i - 1][j] <= 1) {
                grid[i - 1][j] = 0
                dfs(i - 1, j, c[i - 1][j])
            } else if (j > 0 && c[i][j] - c[i][j - 1] <= 1) {
                grid[i][j - 1] = 0
                dfs(i, j - 1, c[i][j - 1])
            }
        }
        dfs(grid.size - 1, grid.size - 1, c.last().last())
    
        for (i in c.indices) for (j in c.indices) c[i][j] = 0

        queue.addLast(intArrayOf(grid.size - 1, grid.size - 1))
        while (queue.isNotEmpty()) {
            repeat(queue.size) {
                val node = queue.removeFirst()
                val i = node[0]
                val j = node[1]
                if (i > 0 && grid[i - 1][j] != -1) {
                    queue.addLast(intArrayOf(i - 1, j))
                    c[i - 1][j] = Math.max(c[i - 1][j], c[i][j] + if (grid[i - 1][j] == 0) 0 else 1)
                } 

                if (j > 0 && grid[i][j - 1] != -1) {
                    queue.addLast(intArrayOf(i, j - 1))
                    c[i][j - 1] = Math.max(c[i][j - 1], c[i][j] + if (grid[i][j - 1] == 0) 0 else 1)
                }
            }
        }


        return first + c[0][0]
    }
}

在如下的case中翻车了。很明显,单次最优并不代表全局最优。

图片.png

题解的思路其实也不算复杂。

由于回程实际上是去程的逆操作,所以我们可以将去程与回程看作两个同时发生的去程。而因为题目限制了移动方向,所以无论是走哪一条路,到达终点所走的步数肯定都是一样的。而由于一个维度的步数等于总步数减去另一个维度的步数,所以可以只记录其中的一个维度。

所以在某个时间点,忽略走到边缘的情况,则其最优值应该由4种情况转移而来,即两个去程分别向右走和向下走。如果两个去程发生碰撞,则只记录一次摘取。

AC代码

class Solution {
    fun cherryPickup(grid: Array<IntArray>): Int {
        val dp = Array<Array<IntArray>>(grid.size * 2 - 1) { Array<IntArray>(grid.size) { IntArray(grid.size) { Int.MIN_VALUE } } }
        dp[0][0][0] = grid[0][0]
        for (k in 1 until grid.size * 2 - 1) {
            for (x1 in Math.max(k - grid.size + 1, 0) .. Math.min(k, grid.size - 1)) {
                if (grid[x1][k - x1] == -1) continue
                for (x2 in x1 .. Math.min(k, grid.size - 1)) {
                    if (grid[x2][k - x2] == -1) continue
                    var res = dp[k - 1][x1][x2]
                    if (x1 > 0) res = Math.max(res, dp[k - 1][x1 - 1][x2])
                    if (x2 > 0) res = Math.max(res, dp[k - 1][x1][x2 - 1])
                    if (x1 > 0 && x2 > 0) res = Math.max(res, dp[k - 1][x1 - 1][x2 - 1])
                    res += grid[x1][k - x1]
                    if (x1 != x2) res += grid[x2][k - x2]
                    dp[k][x1][x2] = res
                }
            }
        }
        if (dp.last().last().last() > 0) return dp.last().last().last()
        return 0
    }
}

时间复杂度:O(n^3)

空间复杂度:O(n^3)

由于k一定是由k - 1转移而来,所以可以进行空间优化,由后向前。

class Solution {
    fun cherryPickup(grid: Array<IntArray>): Int {
        val dp = Array<IntArray>(grid.size) { IntArray(grid.size) { Int.MIN_VALUE } } 
        dp[0][0] = grid[0][0]
        for (k in 1 until grid.size * 2 - 1) {
            for (x1 in Math.min(k, grid.size - 1) downTo Math.max(k - grid.size + 1, 0)) {
                for (x2 in Math.min(k, grid.size - 1) downTo x1) {
                    if (grid[x1][k - x1] == -1 || grid[x2][k - x2] == -1) {
                        dp[x1][x2] = Int.MIN_VALUE
                        continue
                    }
                    var res = dp[x1][x2]
                    if (x1 > 0) res = Math.max(res, dp[x1 - 1][x2])
                    if (x2 > 0) res = Math.max(res, dp[x1][x2 - 1])
                    if (x1 > 0 && x2 > 0) res = Math.max(res, dp[x1 - 1][x2 - 1])
                    res += grid[x1][k - x1]
                    if (x1 != x2) res += grid[x2][k - x2]
                    dp[x1][x2] = res
                }
            }
        }
        if (dp.last().last() > 0) return dp.last().last()
        return 0
    }
}

时间复杂度:O(n^3)

空间复杂度:O(n^2)