题目地址:
https://www.lintcode.com/problem/cherry-pickup/description
给定一个二维 N × N N\times N N×N的矩阵 A A A,只含 0 , 1 , − 1 0,1,-1 0,1,−1三个数。 0 0 0代表空地, − 1 -1 −1代表障碍物, 1 1 1代表樱桃。要求从 ( 0 , 0 ) (0,0) (0,0)出发,每次只能向右或者向下走一格,一路走到 ( N − 1 , N − 1 ) (N-1,N-1) (N−1,N−1),接着再从 ( N − 1 , N − 1 ) (N-1,N-1) (N−1,N−1)出发,每次只能向左或者向上走一格,一路走到 ( 0 , 0 ) (0,0) (0,0)。当走到樱桃上时可以进行采摘,但同一个位置的樱桃只能被采摘一次。问两次走完最多可以采摘多少樱桃。当然路径上不允许有障碍物。如果不存在合法路径,则返回 0 0 0。
思路是动态规划。问题可以转化为两轮从
(
0
,
0
)
(0,0)
(0,0)出发,每次向右或者向下走一格,一路走到
(
N
−
1
,
N
−
1
)
(N-1,N-1)
(N−1,N−1)。因为路径是可逆的,所以这样的转换是可行的。接下来考虑简单的情形,如果只允许走
1
1
1论,那么就是普通的Path Sum问题,可以定义
f
[
x
]
[
y
]
f[x][y]
f[x][y]为从
(
0
,
0
)
(0,0)
(0,0)出发到
(
x
,
y
)
(x,y)
(x,y)为止,可以采摘到的樱桃总数最大值。如果不存在合法路径到达
(
x
,
y
)
(x,y)
(x,y)则规定
f
[
x
]
[
y
]
=
−
∞
f[x][y]=-\infty
f[x][y]=−∞。这样从
(
0
,
0
)
(0,0)
(0,0)出发到
(
x
,
y
)
(x,y)
(x,y)的所有路径可以分为两部分,一部分是从
(
x
−
1
,
y
)
(x-1,y)
(x−1,y)走过来的,一部分是从
(
x
,
y
−
1
)
(x,y-1)
(x,y−1)走过来的。这两部分路径采摘樱桃的更大者就决定了
f
[
x
]
[
y
]
f[x][y]
f[x][y]。所以有:
f
[
x
]
[
y
]
=
max
{
f
[
x
−
1
]
[
y
]
,
f
[
x
]
[
y
−
1
]
}
+
A
[
x
]
[
y
]
f[x][y]=\max\{f[x-1][y],f[x][y-1]\}+A[x][y]
f[x][y]=max{f[x−1][y],f[x][y−1]}+A[x][y]现在问题变成了走两次,我们也可以看成两个人同时走,这样可以类似考虑当第一个人走到
(
x
1
,
y
1
)
(x_1,y_1)
(x1,y1)、同时第二个人走到
(
x
2
,
y
2
)
(x_2,y_2)
(x2,y2)的时候,采摘樱桃的最大数,定义为
f
[
x
1
]
[
y
1
]
[
x
2
]
[
y
2
]
f[x_1][y_1][x_2][y_2]
f[x1][y1][x2][y2]。我们最后关心的就是
f
[
N
−
1
]
[
N
−
1
]
[
N
−
1
]
[
N
−
1
]
f[N-1][N-1][N-1][N-1]
f[N−1][N−1][N−1][N−1]。
但本质上,我们可以只关心使得
x
1
+
y
1
=
x
2
+
y
2
x_1+y_1=x_2+y_2
x1+y1=x2+y2的那些状态,这些状态形成了一个有向无环图(DAG,也就是可以拓扑排序的图,也就可以做动态规划了)。我们可以另外定义
g
[
k
]
[
x
1
]
[
x
2
]
=
f
[
x
1
]
[
k
−
x
1
]
[
x
2
]
[
k
−
x
2
]
g[k][x_1][x_2]=f[x_1][k-x_1][x_2][k-x_2]
g[k][x1][x2]=f[x1][k−x1][x2][k−x2],其中
k
=
x
1
+
y
1
=
x
2
+
y
2
k=x_1+y_1=x_2+y_2
k=x1+y1=x2+y2。具体含义是,两个人从
(
0
,
0
)
(0,0)
(0,0)出发,齐头并进,每次同时走一步,分别到达
(
x
1
,
y
1
)
(x_1,y_1)
(x1,y1)和
(
x
2
,
y
2
)
(x_2,y_2)
(x2,y2)时樱桃最大数。这样最后我们的目标就是求出
g
[
2
N
−
2
]
[
N
−
1
]
[
N
−
1
]
g[2N-2][N-1][N-1]
g[2N−2][N−1][N−1]。而这样的路径可以分为四个部分,第一个人可以从
(
x
1
−
1
,
y
1
)
(x_1-1,y_1)
(x1−1,y1)或者
(
x
1
,
y
1
−
1
)
(x_1,y_1-1)
(x1,y1−1)走过去,第二个人可以从
(
x
2
−
1
,
y
2
)
(x_2-1,y_2)
(x2−1,y2)或者
(
x
2
,
y
2
−
1
)
(x_2,y_2-1)
(x2,y2−1)走过去。如果走到了相同的格子上,则樱桃只采摘一次,避免掉重复计算即可。这样组合一下就是四种情况。将所有情况取个最大值即可。也就是说,若令:
c
=
max
{
g
[
k
−
1
]
[
x
1
−
1
]
[
x
2
−
1
]
,
g
[
k
−
1
]
[
x
1
−
1
]
[
x
2
]
,
g
[
k
−
1
]
[
x
1
]
[
x
2
−
1
]
,
g
[
k
−
1
]
[
x
1
]
[
x
2
]
}
c=\max\{g[k-1][x_1-1][x_2-1],\\g[k-1][x_1-1][x_2],g[k-1][x_1][x_2-1],g[k-1][x_1][x_2]\}
c=max{g[k−1][x1−1][x2−1],g[k−1][x1−1][x2],g[k−1][x1][x2−1],g[k−1][x1][x2]}则当
x
1
=
x
2
x_1=x_2
x1=x2,
y
1
=
y
2
y_1=y_2
y1=y2(也就是走到相同格子上)的时候,
g
[
k
]
[
x
1
]
[
x
2
]
=
c
+
A
[
x
1
]
[
y
1
]
g[k][x_1][x_2]=c+A[x_1][y_1]
g[k][x1][x2]=c+A[x1][y1]当没走到相同格子上的时候,
g
[
k
]
[
x
1
]
[
x
2
]
=
c
+
A
[
x
1
]
[
y
1
]
+
A
[
x
2
]
[
y
2
]
g[k][x_1][x_2]=c+A[x_1][y_1]+A[x_2][y_2]
g[k][x1][x2]=c+A[x1][y1]+A[x2][y2]按照
k
k
k逐层更新这个三维数组即可。代码如下:
public class Solution {
/**
* @param grid: a grid
* @return: the maximum number of cherries possible
*/
public int cherryPickup(int[][] grid) {
// Write your code here
if (grid == null || grid.length == 0 || grid[0].length == 0) {
return 0;
}
int len = grid.length;
int[][][] dp = new int[2 * len - 1][len][len];
dp[0][0][0] = grid[0][0];
for (int s = 1; s < dp.length; s++) {
for (int x1 = 0; x1 < len; x1++) {
for (int x2 = 0; x2 < len; x2++) {
int y1 = s - x1, y2 = s - x2;
if (0 <= y1 && y1 < len && 0 <= y2 && y2 < len) {
// 先初始化为负无穷,表示不存在合法路径
dp[s][x1][x2] = Integer.MIN_VALUE;
// 如果当前其中一个人走到了障碍物上,则不更新这个状态,维持负无穷
if (grid[x1][y1] == -1 || grid[x2][y2] == -1) {
continue;
}
// 取出第一个人走到格子的樱桃数
int ch = grid[x1][y1];
// 如果两个人走到了不同格子上
if (x1 != x2) {
ch += grid[x2][y2];
}
// 为了书写方便,先用tmp存储dp在当前状态的更新
int tmp = 0;
// 这个变量记录dp[s][x1][x2]是否被更新
boolean updated = false;
// up up,两个人都是从上面走过来的,并且上一个状态对应一条合法路径
if (x1 >= 1 && x2 >= 1 && dp[s - 1][x1 - 1][x2 - 1] >= 0) {
tmp = Math.max(tmp, dp[s - 1][x1 - 1][x2 - 1] + ch);
// 标记更新与否为true
updated = true;
}
// up left
if (x1 >= 1 && dp[s - 1][x1 - 1][x2] >= 0) {
tmp = Math.max(tmp, dp[s - 1][x1 - 1][x2] + ch);
updated = true;
}
// left up
if (x2 >= 1 && dp[s - 1][x1][x2 - 1] >= 0) {
tmp = Math.max(tmp, dp[s - 1][x1][x2 - 1] + ch);
updated = true;
}
// left left
if (dp[s - 1][x1][x2] >= 0) {
tmp = Math.max(tmp, dp[s - 1][x1][x2] + ch);
updated = true;
}
// 如果可以被更新,就更新之
if (updated) {
dp[s][x1][x2] = tmp;
}
}
}
}
}
return Math.max(dp[dp.length - 1][len - 1][len - 1], 0);
}
}
时空复杂度 O ( N 3 ) O(N^3) O(N3)。
注解:
updated
变量主要用来处理走不通的情况。如果直接更新dp
数组而不去记录updated
变量的话,就会造成当调用max
的时候,dp
在此处的值会由负无穷变为负无穷加上一个正数,由于计算机里无法真的表示负无穷,所以这样的更新有可能导致不够准确。