【Lintcode】1058. Cherry Pickup

题目地址:

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) (N1,N1),接着再从 ( N − 1 , N − 1 ) (N-1,N-1) (N1,N1)出发,每次只能向左或者向上走一格,一路走到 ( 0 , 0 ) (0,0) (0,0)。当走到樱桃上时可以进行采摘,但同一个位置的樱桃只能被采摘一次。问两次走完最多可以采摘多少樱桃。当然路径上不允许有障碍物。如果不存在合法路径,则返回 0 0 0

思路是动态规划。问题可以转化为两轮从 ( 0 , 0 ) (0,0) (0,0)出发,每次向右或者向下走一格,一路走到 ( N − 1 , N − 1 ) (N-1,N-1) (N1,N1)。因为路径是可逆的,所以这样的转换是可行的。接下来考虑简单的情形,如果只允许走 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) (x1,y)走过来的,一部分是从 ( x , y − 1 ) (x,y-1) (x,y1)走过来的。这两部分路径采摘樱桃的更大者就决定了 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[x1][y],f[x][y1]}+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[N1][N1][N1][N1]
但本质上,我们可以只关心使得 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][kx1][x2][kx2],其中 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[2N2][N1][N1]。而这样的路径可以分为四个部分,第一个人可以从 ( x 1 − 1 , y 1 ) (x_1-1,y_1) (x11,y1)或者 ( x 1 , y 1 − 1 ) (x_1,y_1-1) (x1,y11)走过去,第二个人可以从 ( x 2 − 1 , y 2 ) (x_2-1,y_2) (x21,y2)或者 ( x 2 , y 2 − 1 ) (x_2,y_2-1) (x2,y21)走过去。如果走到了相同的格子上,则樱桃只采摘一次,避免掉重复计算即可。这样组合一下就是四种情况。将所有情况取个最大值即可。也就是说,若令: 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[k1][x11][x21],g[k1][x11][x2],g[k1][x1][x21],g[k1][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在此处的值会由负无穷变为负无穷加上一个正数,由于计算机里无法真的表示负无穷,所以这样的更新有可能导致不够准确。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值