【动态规划--0-1背包变体】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 =
[[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颗樱桃,这是可以摘到的最大值了。

解题思路

8月17日华为机试300分第三题,原题内容是码头,其实与摘樱桃属于换汤不换药的关系,现在复盘以下。

题目要求向右或向下的前进,以及向上或向左的返回,其实等价于前进两次。个人认为,这个等价转换才是本题的难点,想明白了这一点,后续通过代码实现起来就比较easy啦。

那么此时这道题就非常的像0-1背包问题了,或者说是背包问题的变种。背包的大小就是从(0, 0)走到(N - 1, N - 1)需要的 2 * (N - 1) 步。物体的大小即为走过当前樱桃地所需的 1 步,价值即其拥有的樱桃个数。每个樱桃地的樱桃只能采摘一次,也就是0-1背包嘛,求容量为2 * (N - 1)的背包在题目要求的行动规则下能装多少樱桃。

再考虑这么一个问题,如何实现同一个樱桃地的樱桃只能采摘一次的逻辑呢?

为了解决这一问题,同时方便后续dp数组的设计与遍历,我们考虑将两次前进同时进行,即有两个人同时从(0, 0)出发走向(N - 1, N - 1)。两人相遇时,只增加一次樱桃数量。

为什么是两人相遇呢,两人一前一后的到达同一个樱桃地不用考虑吗?事实是不可能出现这种情况,两人一定是同一时间下才可能访问同一片樱桃地。因为在只能向右、向下的行动规则下,每片樱桃地距离起点(0, 0)的距离是一定、唯一的,两人行动的过程中,我们所维护的 k 既可以理解为时间也可以理解为步数。如何在 k 时,第一个人到达A地,那么这也是第二个人能够到达A地的唯一机会,一旦错过,在新的K,K > k下,第二个人是不可能在花费K步的前提下,到达只需k步的A地。

所以对于如何实现同一个樱桃地的樱桃只能采摘一次的逻辑的问题,我们只需在遍历k时判断是否相遇即可。

剩下就是常见的动态规划四步曲:

  • 定义dp数组以及意义: 三维dp数组,dp[k][x1][x2]。其中k是当前走过k步,x1是第一个人的x值,其对应的y1k - x1。同理,理解x2。那么dp[k][x1][x2]的意义为,行动k步后,两人分别抵达(x1, k - x1),(x2, k - x2)时采摘的最多樱桃个数。
  • 确定状态转移公式: 此时的(x1, x2)可能由两人分别同时向右同时向下一右一下一下一右得到。对应着dp[k - 1][x1][x2]dp[k - 1][x1 - 1][x2 - 1]dp[k - 1][x1][x2 - 1]dp[k - 1][x1 - 1][x2]。后续,需要判断是否相遇,如果相遇,只需加一次樱桃个数,不相遇在上述四种可能的最大值的前提下,加上grid[x1][k - x1] + grid[x2][k - x2]
  • 初始化dp数组: 根据状态转移公式判断,所有值最终都依赖dp[0][0][0]的值,其值等于grid[0][0]。此外,剩下所有位置设置为INT_MIN,方便遇到荆棘时的处理,可以continue跳过,相当于对是荆棘的地方设置惩罚项,使其在参与取最大值的比较中自动落选。
  • 确定dp数组的遍历顺序: 当前点依赖该点的上以及左,故遍历时自上而下,自左到右。

下面给出具体代码实现。

代码实现

class Solution {
    
    
    const int dx1[4] = {
    
    0, -1, 0, -1};
    const int dx2[4] = {
    
    0, -1, -1, 0};
public:
    int cherryPickup(vector<vector<int>>& grid) {
    
    
        int n = grid.size();
        //1. 定义dp[k][x1][x2],两人同时从(0,0)出发,各走k步,分别到达(x1, k - x1)、(x2, k - x2)时所摘到的樱桃最大值
        vector<vector<vector<int>>> dp(2 * n - 1, vector<vector<int>>(n, vector<int>(n, INT_MIN)));
        //2. 确定状态转移方程,见上文
        //3. 初始化dp数组
        dp[0][0][0] = grid[0][0];
        //4. 确定遍历顺序
        for(int k = 1; k < 2 * n - 1; k++){
    
    
            for(int x1 = max(k - n + 1, 0); x1 <= min(k, n - 1); x1++){
    
    
                if(grid[x1][k - x1] == -1) continue;
                for(int x2 = x1; x2 <= min(k, n - 1); x2++){
    
    
                    if(grid[x2][k - x2] == -1) continue;
                    for(int i = 0; i < 4; i++){
    
    
                        int nx1 = x1 + dx1[i];
                        int nx2 = x2 + dx2[i];
                        if(nx1 >= 0 && nx2 >= 0){
    
    
                            dp[k][x1][x2] = max(dp[k][x1][x2], dp[k - 1][nx1][nx2]);
                        }
                    }
                    if(x1 != x2) dp[k][x1][x2] += (grid[x1][k - x1] + grid[x2][k - x2]);
                    else dp[k][x1][x2] += grid[x1][k - x1];
                }
            }
        }
        return max(dp[2 * n - 2][n - 1][n - 1], 0);//存在起点与终点之间完全被荆棘隔开,但凡通过荆棘的路径其值都为负数。
    }
};

运行结果:
在这里插入图片描述
后续,可以考虑利用滚动数组对内存占用进行优化,具体讲优化掉第一维,如下:

class Solution {
    
    
    const int dx1[4] = {
    
    0, -1, 0, -1};
    const int dx2[4] = {
    
    0, -1, -1, 0};
public:
    int cherryPickup(vector<vector<int>>& grid) {
    
    
        int n = grid.size();
        //1. 定义dp[x1][x2],两人同时从(0,0)出发,分别到达(x1, k - x1)、(x2, k - x2)时所摘到的樱桃最大值
        vector<vector<int>> dp(n, vector<int>(n, INT_MIN));
        //2. 确定状态转移方程,见上文
        //3. 初始化dp数组
        dp[0][0] = grid[0][0];
        //4. 确定遍历顺序
        for(int k = 1; k < 2 * n - 1; k++){
    
    
            for(int x1 = min(k, n - 1); x1 >= max(k - n + 1, 0); x1--){
    
    
                for(int x2 = min(k, n - 1); x2 >= x1; x2--){
    
    
                    //对荆棘点的处理发生了改变
                    if(grid[x1][k - x1] == -1 || grid[x2][k - x2] == -1){
    
    
                        dp[x1][x2] = INT_MIN;
                        continue;
                    }
                    int res = INT_MIN;
                    for(int i = 0; i < 4; i++){
    
    
                        int nx1 = x1 + dx1[i];
                        int nx2 = x2 + dx2[i];
                        if(nx1 >= 0 && nx2 >= 0){
    
    
                            res = max(res, dp[nx1][nx2]);
                        }
                    }
                    if(x1 != x2) res += (grid[x1][k - x1] + grid[x2][k - x2]);
                    else res += grid[x1][k - x1];
                    dp[x1][x2] = res;
                }
            }
        }
        return max(dp[n - 1][n - 1], 0);
    }
};

优化后:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/LogosTR_/article/details/126545137