【C/C++】741. 摘樱桃

120 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第18天,点击查看活动详情


题目链接: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) 之间不存在一条可经过的路径,则没有任何一个樱桃能被摘到。

提示:

  • grid 是一个 N * N 的二维数组,N 的取值范围是 1N501 \leqslant N \leqslant 50
  • 每一个 grid[i][j] 都是集合 {-1, 0, 1} 其中的一个数。
  • 可以保证起点 grid[0][0] 和终点 grid[N-1][N-1] 的值都不会是 -1

示例 1:

输入: grid =
[[0, 1, -1],
 [1, 0, -1],
 [1, 1,  1]]
输出: 5
解释: 
玩家从(0,0)点出发,经过了向下走,向下走,向右走,向右走,到达了点(2, 2)。
在这趟单程中,总共摘到了4颗樱桃,矩阵变成了[[0,1,-1],[0,0,-1],[0,0,0]]。
接着,这名玩家向左走,向上走,向上走,向左走,返回了起始点,又摘到了1颗樱桃。
在旅程中,总共摘到了5颗樱桃,这是可以摘到的最大值了。

整理题意

题目给定一个 n * n 的二维矩阵 grid 表示一块樱桃地,矩阵中仅包含 01-1 三种标记:

  • 0 表示空地,可以通过。
  • 1 表示樱桃,可以通过,并可以获得樱桃,获得后该地标记变为 0
  • -1 表示无法通过的地方。 我们从 [0, 0] 出发,只能向下和向右移动,直至移动到 [n - 1, n - 1],然后再从 [n - 1, n - 1] 出发,只能向上和向左移动,直至移动到 [0, 0],问这么一来一回最大获得的樱桃数量是多少。

题目规定如果无法到达右下角 [n - 1, n - 1],说明没有路径,规定此时没有任何一个樱桃能被摘到,返回 0

解题思路分析

根据题目描述的从右下角出发到左上角的过程可以逆向看作再次从左上角出发到右下角的过程,也就是从 (n-1, n-1) 返回 (0, 0) 的这条路径可以等价地看成从 (0, 0) 到 (n-1, n-1) 的路径,因此问题可以等价转换成:有两个人从 (0, 0) 出发,向下或向右走到 (n-1, n-1) 时,摘到的樱桃个数之和的最大值。

那我们可以定义状态为:dp[x1][y1][x2][y2] 表示两个人从 (0,0) 出发分别到达 (x1, y1)(x2, y2) 点时所摘得樱桃的最大值。

由于两人速度相同,且同时从起点 (0,0) 出发,所以我们可以得到 x1+y1=x2+y2=kx1​+y1​=x2​+y2​=kk 为所走的步数,所以可以将定义状态优化为:dp[k][x1][x2] 表示两个人从 (0,0) 出发分别到达 (x1, k - x1)(x2, k - x2) 点时所摘得樱桃的最大值。

考虑状态转移方程,由于当前两个位置只可能分别从其左方向和上方向转移过来,所以总共有四个组合,枚举两个位置上一步的走法来计算 dp[k][x1][x2]dp[k][x1​][x2​]。有四种情况:

  • dp[k1][x1][x2]dp[k-1][x_1][x_2] 转移过来:表示两个位置都由上一步向右转移过来;
  • dp[k1][x11][x2]dp[k-1][x_1-1][x_2] 转移过来:表示 x1 是由上一步往下,x2 是由上一步往右转移过来;
  • dp[k1][x1][x21]dp[k-1][x_1][x_2-1] 转移过来:表示 x1 是由上一步往右,x2 是由上一步往下转移过来;
  • dp[k1][x11][x21]dp[k-1][x_1-1][x_2-1] 转移过来:表示都是由上一步往下转移过来。

取这四种情况的最大值再加上 grid[x1][kx1]\textit{grid}[x_1][k-x_1]grid[x2][kx2]\textit{grid}[x_2][k-x_2] 的值,就得到了 dp[k][x1][x2]dp[k][x_1][x_2],如果 x1=x2x_1=x_2,因为同一块地上的樱桃只能摘取一次,所以只需加一次 grid[x1][kx1]\textit{grid}[x_1][k-x_1]

最后答案为 dp[2n2][n1][n1]dp[2n-2][n-1][n-1],需要注意如果无法到达返回 0

具体实现

  • 首先将 dp 数组初始化为 INT_MIN 表示所有情况不可达,也是为了防止从值为 −1 的格子进行转移影响答案的正确性,然后初始化起点 dp[0][0][0] = gird[0][0]
  • 因为我们可以将两条路径的上轮廓看成是 x1x_1 走出的路径,下轮廓看成是 x2x_2 走出的路径,即视作 x1x_1 始终不会走到 x2x_2 的下方,则有 x1x2x_1\le x_2,在代码实现时保证这一点,可以减少循环次数。
  • 又因为每次状态转移都是由上一步转移过来,所以我们可以通过倒序循环 x1x_1 和 x2x_2,来优化掉第一个维度,但是需要注意优化一个维度后需要倒序循环,这样可以避免覆盖问题造成的错误。

复杂度分析

  • 时间复杂度:O(n3)O(n^3),其中 n 是矩阵 grid 的长宽。
  • 空间复杂度:O(n2)O(n^2),空间优化后为 O(n2)O(n^2)

代码实现

class Solution {
public:
    int cherryPickup(vector<vector<int>>& grid) {
        int n = grid.size();
        int dp[n][n];
        //初始化清空dp数组
        for(int i = 0; i < n; i++){
            for(int j = 0; j < n; j++){
                dp[i][j] = INT_MIN;
            }
        }
        //初始化边界,在 [0, 0] 时的樱桃数量
        dp[0][0] = grid[0][0];
        //因为 k范围为 [0, 2n - 2]
        for(int k = 1; k < 2 * n - 1; k++){
            for(int x1 = min(k, n - 1); x1 >= max(k - n + 1, 0); x1--){
                int y1 = k - x1;
                for(int x2 = min(k, n - 1); x2 >= x1; x2--){
                    int y2 = k - x2;
                    //无法转移到越界或荆棘位置
                    if(y2 < 0 || y2 >= n || grid[x2][y2] == -1
                    || y1 < 0 || y1 >= n || grid[x1][y1] == -1){
                        //优化维度后需要注意将不可达的地方赋值为 INT_MIN;
                        //避免跳步造成的答案错误。
                        dp[x1][x2] = INT_MIN;
                        continue;
                    }
                    int a = dp[x1][x2];
                    int b = INT_MIN;
                    if(x1) b = dp[x1 - 1][x2];
                    int c = INT_MIN;
                    if(x1 && x2) c = dp[x1 - 1][x2 - 1];
                    int d = INT_MIN;
                    if(x2) d = dp[x1][x2 - 1];
                    //四种情况取最大值
                    dp[x1][x2] = max({a, b, c, d}) + grid[x1][y1];
                    //同一个位置只加一次
                    if(x1 != x2) dp[x1][x2] += grid[x2][y2];
                }
            }
        }
        return dp[n - 1][n - 1] < 0 ? 0 : dp[n - 1][n - 1];
    }
};

总结

  • 该题难点在于题目的转换和动态规划的状态定义,其次是优化循环次数和优化维度也是比较困难的地方。
  • 核心思想在于将题目转换为:有两个人从 (0, 0) 出发,向下或向右走到 (n-1, n-1) 时,摘到的樱桃个数之和的最大值
  • 在代码实现过程中需要注意的边界问题,以及在优化后需要注意的覆盖问题等,同时还需要注意不可达的情况,因为维度减少,所以需要将不可达赋值为 INT_MIN,否则可能将会从上上一个状态转移过来。
  • 测试结果:

741.png

结束语

人生是一个循序渐进的过程,你想要拥有强大的内心,就必须度过那些专注成长的日子。当你保持足够的耐心专注投入某件事时,从无到有、从零到一、从低到高,变得有意义的不止是内心,还有我们的人生。新的一天,加油!