题目描述

✅ 741. 摘樱桃

image-20260929104700416

image-20260929104700552

image-20260929104700725

题意分析

在方阵中从左上角出发,只向右或向下走到右下角,再只向左或向上返回。-1 是不能经过的障碍,0 是空格,1 是只能采摘一次的樱桃。求往返合计最多能摘多少;没有完整可行路径时返回 0。

解法:同步双路径动态规划

核心思路

[!blue]

把回程反向看,它也是一条从左上到右下、只向右或向下的路径。因此问题等价于两个人从起点同步走到终点,最大化两条路径覆盖的樱桃总数,同一格只计算一次。两条路径允许重合,不能先单独选一条最优去程再固定它求回程。

走了 t 步后,任何人的位置都满足 row+col = t。固定步数时只需记录两人的行号 r1、r2,列号由 c1 = t-r1、c2 = t-r2 得到。当前层的 ndp[r1][r2] 保存两人到达这两个位置的最大收益,dp 则保存第 t-1 层,供当前层读取前驱。

每个人只能从左侧或上方到达当前格,前一行号分别是 r 或 r-1,所以两人共有四种前驱组合。取其中可达状态的最大收益,再加当前格的樱桃:若 r1 == r2,两人列号也相等,只加一格;否则加两格。位置越界或任意一人遇到障碍时,这个状态不可达。

只在同一步检查重合就能完成整条路径的去重,因为同一个格子的行列和固定,两人若都经过它,一定在同一步到达。未来步数更大,也不可能再次经过已计入的格子,因此同一状态只保留最大历史收益,不需要记录此前摘过哪些樱桃。

初始两人同在起点,只有 dp[0][0] = grid[0][0] 可达,其余位置设为负无穷;题目保证起点和终点不是障碍。每层重新建立全为负无穷的 ndp,只从可达前驱转移,避免把不可达状态当作收益为 0 的路径。行号范围由 0 <= r < n 和 0 <= t-r < n 共同确定,即 [max(0, t-n+1), min(n-1, t)]。

两人走完 2*(n-1) 步时都应位于终点,答案为 dp[n-1][n-1];它若仍不可达就返回 0。只有一个格子时无需转移,起点樱桃也只计一次。

解题步骤

  1. 初始化两人同在起点的状态。
  2. 按步数逐层创建不可达的新状态表。
  3. 枚举两人的合法行号,跳过障碍,并从四种前驱取得最大值。
  4. 增加本步新增樱桃数,结束后将无法到达终点的结果转换为零。

代码实现

class Solution {
    public int cherryPickup(int[][] grid) {
        int n = grid.length;
        int negInf = Integer.MIN_VALUE / 4;

        int[][] dp = new int[n][n];

        for (int[] row : dp) {
            Arrays.fill(row, negInf);
        }

        dp[0][0] = grid[0][0];

        for (int t = 1; t <= 2 * (n - 1); t++) {
            int[][] ndp = new int[n][n];

            for (int[] row : ndp) {
                Arrays.fill(row, negInf);
            }

            int rMin = Math.max(0, t - (n - 1));
            int rMax = Math.min(n - 1, t);

            for (int r1 = rMin; r1 <= rMax; r1++) {
                for (int r2 = rMin; r2 <= rMax; r2++) {
                    // 同步步数等于行列下标之和,记录行号即可推出列号。
                    int c1 = t - r1;
                    int c2 = t - r2;

                    if (c1 < 0 || c1 >= n || c2 < 0 || c2 >= n) {
                        continue;
                    }

                    if (grid[r1][c1] == -1 || grid[r2][c2] == -1) {
                        continue;
                    }

                    int cherries = grid[r1][c1];

                    // 同一步行号相同就位于同一格,该格樱桃只计一次。
                    if (r1 != r2) {
                        cherries += grid[r2][c2];
                    }

                    // 两人各自来自上方或左方,共枚举四种前驱组合。
                    int best = negInf;

                    for (int dr1 = 0; dr1 <= 1; dr1++) {
                        for (int dr2 = 0; dr2 <= 1; dr2++) {
                            int pr1 = r1 - dr1;
                            int pr2 = r2 - dr2;

                            if (pr1 < 0 || pr2 < 0) {
                                continue;
                            }

                            best = Math.max(best, dp[pr1][pr2]);
                        }
                    }

                    if (best != negInf) {
                        ndp[r1][r2] = Math.max(ndp[r1][r2], best + cherries);
                    }
                }
            }

            dp = ndp;
        }

        return Math.max(0, dp[n - 1][n - 1]);
    }
}
func cherryPickup(grid [][]int) int {
    n := len(grid)
    const negInf = -1 << 60

    dp := make([][]int, n)
    for i := range dp {
        dp[i] = make([]int, n)
        for j := range dp[i] {
            dp[i][j] = negInf
        }
    }
    dp[0][0] = grid[0][0]

    for t := 1; t <= 2*(n-1); t++ {
        ndp := make([][]int, n)
        for i := range ndp {
            ndp[i] = make([]int, n)
            for j := range ndp[i] {
                ndp[i][j] = negInf
            }
        }

        rMin := max741(0, t-(n-1))
        rMax := min741(t, n-1)

        for r1 := rMin; r1 <= rMax; r1++ {
            for r2 := rMin; r2 <= rMax; r2++ {
                // 同步步数等于行列下标之和,记录行号即可推出列号。
                c1 := t - r1
                c2 := t - r2

                if c1 >= n || c2 >= n || grid[r1][c1] == -1 || grid[r2][c2] == -1 {
                    continue
                }

                cherries := grid[r1][c1]
                // 同一步行号相同就位于同一格,该格樱桃只计一次。
                if r1 != r2 {
                    cherries += grid[r2][c2]
                }

                // 两人各自来自上方或左方,共枚举四种前驱组合。
                best := negInf
                for _, dr1 := range []int{
                    0,
                    1,
                } {
                    for _, dr2 := range []int{
                        0,
                        1,
                    } {
                        pr1, pr2 := r1-dr1, r2-dr2
                        if pr1 >= 0 && pr2 >= 0 && dp[pr1][pr2] != negInf {
                            if dp[pr1][pr2] > best {
                                best = dp[pr1][pr2]
                            }
                        }
                    }
                }

                if best != negInf && best+cherries > ndp[r1][r2] {
                    ndp[r1][r2] = best + cherries
                }
            }
        }
        dp = ndp
    }

    if dp[n-1][n-1] < 0 {
        return 0
    }
    return dp[n-1][n-1]
}

func max741(a, b int) int {
    if a > b {
        return a
    }
    return b
}

func min741(a, b int) int {
    if a < b {
        return a
    }
    return b
}

复杂度分析

  • 时间复杂度:$O(n^3)$,n 为方阵边长,步数为 $O(n)$,每层枚举 $O(n^2)$ 对位置并检查四个前驱。
  • 空间复杂度:$O(n^2)$,仅保留相邻两层状态。

关键点总结

[!green]

  • 同步步数消去两个列坐标,将状态降到三维并滚动存储。
  • 两条路径可以重合,限制只是同一樱桃不能重复计数。
  • 不可达状态与合法的零收益状态需要区分。

易错点总结

[!yellow]

  • 先贪心选去程,再求最佳回程:去程局部最优不保证两条路径并集最大。
  • 同格仍把两人的樱桃都相加:重复计数。
  • 原地更新本层状态:可能混入已经属于当前步数的结果。
  • 把没有完整路径的负哨兵直接返回:题目要求此时返回零。

相似题目

题目 难度 关联与区别
1463. 摘樱桃 II 困难 同样用两名同步移动者避免独立路径重复计分,原题每步向下一行,本题按相同总步数同步。
64. 最小路径和 中等 单条最优路径DP是基础,本题两条路径可能共享格子且只能计一次,不能独立求两次最优。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2022/95731090
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!