一、题目
一个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。
二、代码
class Solution {
public static int cherryPickup(int[][] grid) {
// 矩阵的行数
int n = grid.length;
// 矩阵的列数
int m = grid[0].length;
// dp缓存
// dp[i][j][k]:表示A来到(i,j),B来到(k,i + j - k)时,能收集到的最多樱桃数量
int[][][] dp = new int[n][m][n];
// 先给dp缓存赋初值,这里设置为系统最小值,用来表示当前位置的值还从来没有计算过
// 其实只要是赋值小于-1的数用来标记该位置的值还没有计算过就可以,因为题目中有意义的值都是大于等于-1的
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
for (int k = 0; k < n; k++) {
dp[i][j][k] = Integer.MIN_VALUE;
}
}
}
// 开始递归,A和B同时从左上角出发
int ans = process(0, 0, 0, grid, n, m, dp);
// 如果返回-1,就说明根本无法从起点走到终点再走回去,这种情况就认为能拿到0个樱桃
return ans == -1 ? 0 : ans;
}
// 此时A来到(ai,aj),B来到(bi,ai + aj - bi),求此时已经能拿到的最大樱桃数。返回-1表示根本无法从起点走到终点再走回去
// 这里只用了三个可变参数表示A和B的坐标,因为A和B都是只能向下或者向右走,并且他们是同步移动的,所以他们两个走的步数一定一直都是一样的,所以只要是知道了三个坐标数,就可以推出来下一个
public static int process(int ai, int aj, int bi, int[][] grid, int n, int m, int[][][] dp) {
// 省一个参数,因为A和B是同步走的,所以他们走过的步数一定是一样的,又因为他们只能向下走或者向右走,所以他们走的步数就是纵坐标+横坐标。
// 所以他们的坐标满足a+b=c+d的关系,也就是说只要是知道了三个数,就可以推出第四个数,这就省掉了一个可变参数,提高了效率。
// 计算B的列下标
int bj = ai + aj - bi;
// 判断此时位置是否越界,如果越界直接返回系统最小值,表示这个位置根本就是无效的,也就不用为其赋值
if (ai >= n || bi >= n || aj >= m || bj >= m) {
return Integer.MIN_VALUE;
}
// 如果该位置在缓存中已经有了结果了,直接取出来返回
if (dp[ai][aj][bi] != Integer.MIN_VALUE) {
return dp[ai][aj][bi];
}
// 如果此时两个点已经到达了右下角重点,此时两个点一定是同时到达的,所以只能取一次樱桃,将这个位置的樱桃树复制到dp,并返回
// 其实这里只判断ai == n - 1 && aj == m - 1即可,因为A和B一定能同时到达终点
if (ai == n - 1 && aj == m - 1 && bi == n - 1 && bj == m - 1) {
dp[ai][aj][bi] = grid[ai][aj];
return dp[ai][aj][bi];
}
// 尝试全部有可能的四种移动方式,通过递归得到这几种方式能拿到的最大樱桃树
int p1 = process(ai + 1, aj, bi + 1, grid, n, m, dp);
int p2 = process(ai, aj + 1, bi, grid, n, m, dp);
int p3 = process(ai + 1, aj, bi, grid, n, m, dp);
int p4 = process(ai, aj + 1, bi + 1, grid, n, m, dp);
// 从这四种结果中取最大值,作为后续的走法能拿到的最大樱桃树
int next = Math.max(p1, Math.max(p2, Math.max(p3, p4)));
// 记录当前已经能拿到的最大樱桃树
int cur = 0;
// 如果此时A、B所在位置或者后续返回上来的数有一个是-1,就说明此时这个走法一定是走不通的,不可能走到终点,那么就将该位置也设置为-1,返回
if (grid[ai][aj] == -1 || grid[bi][bj] == -1 || next == -1) {
// 上面三个数存在-1,要么表示当前位置会被-1挡住(也就是去或者回的路会被当前遇到的-1挡住),要么表示后续走过的路会被-1挡住,不管哪一种情况,都意味着当前这个走法是走不通的
dp[ai][aj][bi] = -1;
return dp[ai][aj][bi];
// 走到这个分支,表示当前的路线能走通
} else {
// 如果此时A和B的位置不同,说明两个位置的樱桃出都要累加到当前能拿到的樱桃数中
if (ai != bi || aj != bj) {
cur = grid[ai][aj] + grid[bi][bj];
// 如果A和B此时在同一位置,那么就只能拿一次樱桃
} else {
cur = grid[ai][aj];
}
}
// 将当前能拿到的最大樱桃数量加上后续步骤能拿到的最大樱桃树两
dp[ai][aj][bi] = cur + next;
return dp[ai][aj][bi];
}
}
三、解题思路
我们就假设两个人A、B都从左下角走到右下角,都只能向下或者向右走,但是A和B能做出不同的选择
如果,某一时刻,AB进入相同的一个格子,A和B只获得一份(如果A和B都要通过同一个格子,他们必定同时到达这个格子的,因为他们起点相同,又只能走下或右,所以一定会同时进入到相同的格子),A走到之后,就认为B走过的路径就是回来的路径。
能够省一个参数,因为A和B是同步走的,所以他们走过的步数一定是一样的,又因为他们只能向下走或者向右走,所以他们走的步数就是纵坐标+横坐标。
所以他们的坐标满足a+b=c+d的关系,也就是说只要是知道了三个数,就可以推出第四个数,这就省掉了一个可变参数。