【leetcode】741 摘樱桃(动态规划)

这篇博客介绍了LeetCode第741题‘樱桃采摘’的解题思路,主要通过动态规划的方法求解。博主首先分析了暴力回溯法的时间复杂度,然后探讨了动态规划的尝试,包括修改grid矩阵的DP和不修改grid矩阵的最终版DP。最终版本的动态规划中,博主提出了一个避免重复计数并保持自我一致性(self-consistent)的两腿DP定义,给出了递推关系式和代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目链接:https://leetcode-cn.com/problems/cherry-pickup/

题目描述

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

示例 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颗樱桃,这是可以摘到的最大值了。

说明:

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

思路

题目的详解见英文leetcode原帖:https://leetcode.com/problems/cherry-pickup/discuss/109903/Step-by-step-guidance-of-the-O(N3)-time-and-O(N2)-space-solution

因为题目限制了grid[i][j]=1的节点只能访问一次,所以核心的问题在于要避免partI和partII重复计数。

I 暴力尝试

暴力回溯,指数级时间复杂度。每趟来回 (4N-4)步,可能的往返数 2^(4N-4)

II 动态规划尝试

(0, 0) ==> (N-1, N-1)(N-1, N-1)==>(0,0)分别使用动态规划得到其子问题最优解,但是总体 (0, 0) ==> (N-1, N-1) ==> (0, 0) 不一定是最优解。

grid = [[1,1,1,0,1],
        [0,0,0,0,0],
        [0,0,0,0,0],
        [0,0,0,0,0],
        [1,0,1,1,1]].

如上式在partI可以得到最优解,(0, 0) ==> (0, 2) ==> (4, 2) ==> (4, 4)总和为6,对应的partII最多总和为1,总体来回总和为7。然后沿着矩形边缘可以得到所有的8个樱桃。所以这个贪心策略不一定能得到最优解。

III 修改grid矩阵的动态规划尝试

记录当前grid的状态,空间复杂度太高了

IV 最终版-不修改grid矩阵的动态规划

是否可以缩短路程,不需要走到右下角?YES!

我们重新定义 T(i,j) 为简化路程 (0, 0) ==> (i, j) ==> (0, 0)可以得到的最大数,无需修改输入矩阵。此时原问题可以表示为T(N-1,N-1)。为了得到递推关系式:

对于每个坐标 (i,j),我们有两种方式到达以及两种方式离开该点:(i-1, j)(i, j-1),往返路程可以分为以下四个case:

Case 1: (0, 0) ==> (i-1, j) ==> (i, j) ==> (i-1, j) ==> (0, 0)
Case 2: (0, 0) ==> (i, j-1) ==> (i, j) ==> (i, j-1) ==> (0, 0)
Case 3: (0, 0) ==> (i-1, j) ==> (i, j) ==> (i, j-1) ==> (0, 0)
Case 4: (0, 0) ==> (i, j-1) ==> (i, j) ==> (i-1, j) ==> (0, 0)

根据定义,case1等价于T(i-1, j) + grid[i][j] ,case2等价于T(i, j-1) + grid[i][j]。但是我们定义的T(i, j)并没有覆盖最后两种情形:PartI最后一步和PartII第一步不同。因此我们需要修改T(i,j)定义,扩展为T(i, j, p, q),表明两段路程(0, 0) ==> (i, j); (p, q) ==> (0, 0)的最大樱桃数,无需修改gird矩阵。

类似上文所述,我们有两种方式到达坐标(i,j),两种方式离开坐标(p,q)

Case 1: (0, 0) ==> (i-1, j) ==> (i, j); (p, q) ==> (p-1, q) ==> (0, 0)
Case 2: (0, 0) ==> (i-1, j) ==> (i, j); (p, q) ==> (p, q-1) ==> (0, 0)
Case 3: (0, 0) ==> (i, j-1) ==> (i, j); (p, q) ==> (p-1, q) ==> (0, 0)
Case 4: (0, 0) ==> (i, j-1) ==> (i, j); (p, q) ==> (p, q-1) ==> (0, 0)

根据定义得到:

Case 1 is equivalent to T(i-1, j, p-1, q) + grid[i][j] + grid[p][q];
Case 2 is equivalent to T(i-1, j, p, q-1) + grid[i][j] + grid[p][q];
Case 3 is equivalent to T(i, j-1, p-1, q) + grid[i][j] + grid[p][q];
Case 4 is equivalent to T(i, j-1, p, q-1) + grid[i][j] + grid[p][q];

递推关系式为:

T(i, j, p, q) = grid[i][j] + grid[p][q] + max{T(i-1, j, p-1, q), T(i-1, j, p, q-1), T(i, j-1, p-1, q), T(i, j-1, p, q-1)}
约束条件-避免重复计数

至此,我们需要设置约束来避免对同一格子重复计数。因为上文我们已经在计算T(i, j, p, q)时计数了gird[i][j]gird[p][q],为了避免重复计数,这两个gird节点不应该在 T(i-1, j, p-1, q), T(i-1, j, p, q-1), T(i, j-1, p-1, q) and T(i, j-1, p, q-1)这四个中任何一个的计算中被计数。

显然 (i, j) 不可能出现在 (0, 0) ==> (i-1, j)(0, 0) ==> (i, j-1),同理 (p, q) 不会出现在 (p-1, q) ==> (0, 0)(p, q-1) ==> (0, 0)

因此如果我们可以保证(i, j)不出现在(p-1, q) ==> (0, 0) or (p, q-1) ==> (0, 0) 并且 (p, q) 不出现在 (0, 0) ==> (i-1, j) or (0, 0) ==> (i, j-1),则将不会发生重复计数。怎么做呢?

(0, 0) ==> (i-1, j) and (0, 0) ==> (i, j-1)举例。我们知道这些路径的边界,前者所有路径落在矩形 [0, 0, i-1, j] ,后者所有路径落在矩形[0, 0, i, j-1],这表明两个路径合起来将会落在矩形[0, 0, i, j]除右下角(i,j)的区域。因此如果我们保证 (p, q) 在矩形[0, 0, i, j]之外(除了特殊情况在(i,j)处重叠),它将绝不会出现在(0, 0) ==> (i-1, j) or(0, 0) ==> (i, j-1)的路径上。

同理 (i, j) 也必须落在矩形 [0, 0, p, q] 之外,避免重复计数。总结得到以下三个条件之一应该为true

  1. i < p && j > q
  2. i == p && j == q
  3. i > p && j < q

这说明往返路程T(i, j, p, q)并非对所有四个坐标的取值都有效,而应该满足上述条件。

但是 T(i, j, p, q) 不满足self-consistency, For example, T(3, 1, 2, 3) is valid under these conditions but one of the terms in the recurrence relations, T(2, 1, 2, 2), would be invalid, and we have no idea how to get its value under current definition of T(i, j, p, q).

Self-consistent two-leg DP definition

因此上面四个参数是互相关联的,不是独立参数。上述四个参数的表达式不是最简的,我们希望能够找到满足上述三个条件的子集,以确保一定满足上述条件。我们可以观察到**:当 i(p) 增加时,我们必须让 j(q) 减小使得上式满足,反之亦然**(它们是负关联的)。于是我们可以设置i (p) 和j (q) 的和为某个常数,n = i + j = p + q(Note in this subset of conditions, n can be interpreted as the number of steps from the source position (0, 0). I have also tried other anti-correlated functions for i and j such as their product is a constant but it did not work out. The recurrence relations here play a role and constant sum turns out to be the simplest one that works.)

由此新条件,我们可以重写满足 n = i + j = p + q的式子T(i, j, p, q)为:

T(n, i, p), where T(n, i, p) = T(i, n-i, p, n-p).

T(i-1, n-i, p-1, n-p) = T(n-1, i-1, p-1)
T(i-1, n-i, p, n-p-1) = T(n-1, i-1, p)
T(i, n-i-1, p-1, n-p) = T(n-1, i, p-1)
T(i, n-i-1, p, n-p-1) = T(n-1, i, p)

于是得到递归表达式为:

T(n, i, p) = grid[i][n-i] + grid[p][n-p] + max{T(n-1, i-1, p-1), T(n-1, i-1, p), T(n-1, i, p-1), T(n-1, i, p)}.

Of course, in the recurrence relation above, only one of grid[i][n-i] and grid[p][n-p] will be taken if i == p (i.e., when the two positions overlap). Also note that all four indices, i, j, p and q, are in the range [0, N), meaning n will be in the range [0, 2N-1) (remember it is the sum of i and j). Lastly we have the base case given by T(0, 0, 0) = grid[0][0].

上述式子,时间复杂度:O(n3),空间复杂度:O(n3);但是观察到T(n, i, p) only depends on those subproblems with n - 1,所以空间复杂度可以降为O(n^2)。

代码

/*
 * 动态规划
 * 时间复杂度O(n^3) 空间复杂度O(n^2)
 */

class Solution {
public:
    int cherryPickup(vector<vector<int>>& grid) {
        int N = grid.size(), M = (N<< 1) -1;    // M表示从(0,0)到(N-1,N-1)的格子数
        vector<vector<int>> dp(N, vector<int> (N,0));
        dp[0][0] = grid[0][0];

        for (int n = 1; n < M; ++n) {
            for (int i = N-1; i >= 0; --i) {
                for (int p = N-1; p >=0 ; --p) {
                    int j = n - i, q = n-p;
                    // 出界判断,出现障碍
                    if (j < 0 || j >= N || q < 0 || q >= N || grid[i][j] < 0 || grid[p][q] < 0) {
                        dp[i][p] = -1;
                        continue;
                    }

                    if(i>0) dp[i][p] = max(dp[i][p], dp[i-1][p]);
                    if (p > 0) dp[i][p] = max(dp[i][p], dp[i][p - 1]);
                    if (i > 0 && p > 0) dp[i][p] = max(dp[i][p], dp[i - 1][p - 1]);

                    if (dp[i][p] >= 0) dp[i][p] += grid[i][j] + (i != p ? grid[p][q] : 0);
                }
            }
        }

        return max(dp[N-1][N-1],0);
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值