题目链接: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:
i < p && j > q
i == p && j == q
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);
}
};