题目描述

✅ 剑指 Offer 47. 礼物的最大价值

image-20261001230752572

题意分析

从网格左上角出发,每次只能向右或向下移动一格,直到右下角,沿途经过的格子都可以拿到礼物。每个礼物价值大于零,求一条路径能取得的最大价值总和,起点和终点也计入。

只能选择一条连续路径,不能同时收集两条分支上的礼物。每次只能来自上方或左方,因此当前格子的最优结果取决于这两个相邻前驱。

解法:一维动态规划

核心思路

[!blue]

到达当前格子的最后一步只能是从上面向下,或从左边向右。无论最后一步来自哪一边,都应沿用到达那个前驱的最大价值;若前缀路径还可以更好,当前整条路径也能随之改善。因此当前最优值等于两个前驱中的较大值,再加当前礼物价值。

按行从上到下、每行从左到右计算,两个前驱都已准备好。用一维数组压缩状态:更新列 j 之前,dp[j] 仍是上一行同列的最优值;dp[j - 1] 已经更新,表示当前行左侧的最优值。读取这两个值后覆盖 dp[j],就得到当前格子的答案。

第一列没有左侧前驱,令 left = 0;第一行的上方状态也初始为零。因为礼物价值为正,边界上已经形成的合法路径会优于这个零,故第一行自然从左侧累积,第一列自然从上方累积。左上角两个前驱都为零,恰好得到它自己的价值。

每个格子完成后,一维数组左边部分表示当前行,尚未处理的部分表示上一行。最后一行更新结束时,最后一列就是到右下角的最大价值;原网格无需修改。

解题步骤

  1. 创建长度为列数的零数组 dp。
  2. 从第一行到最后一行处理,每行都从左向右。
  3. 取更新前的 dp[j] 作为上方值,取已经更新的 dp[j - 1] 作为左方值;第一列左方值为零。
  4. 将两者较大值加上 grid[i][j],覆盖 dp[j]。
  5. 全部处理完成后返回最后一列的状态。

代码实现

class Solution {
    public int maxValue(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        int[] dp = new int[n];

        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                int left = 0;

                if (j > 0) {
                    // 左侧已经更新,表示当前行的最优路径。
                    left = dp[j - 1];
                }

                // 覆盖前保存上一行同列的旧值。
                int up = dp[j];

                dp[j] = Math.max(left, up) + grid[i][j];
            }
        }

        return dp[n - 1];
    }
}
func maxValue(grid [][]int) int {
    m, n := len(grid), len(grid[0])
    dp := make([]int, n)

    for i := 0; i < m; i++ {
        for j := 0; j < n; j++ {
            left := 0
            if j > 0 {
                // 左侧已经更新,表示当前行的最优路径。
                left = dp[j-1]
            }
            // 覆盖前保存上一行同列的旧值。
            up := dp[j]
            if left > up {
                dp[j] = left + grid[i][j]
            } else {
                dp[j] = up + grid[i][j]
            }
        }
    }

    return dp[n-1]
}

复杂度分析

设网格有 $m$ 行、$n$ 列。

  • 时间复杂度:$O(mn)$,每个格子只进行一次常数时间转移。
  • 辅助空间复杂度:$O(n)$,只保存一行状态,输入网格不修改。

关键点总结

[!green]

  • 最后一步只有上、左两种来源,取较大前缀而不是把两个分支相加。
  • 一维数组同时承载上一行与当前行,读取方向决定状态含义。
  • 正价值保证零初始化能统一处理起点和首行、首列。

易错点总结

[!yellow]

  • 从右到左更新会读到上一行的左侧状态,无法表达当前行的左方前驱。
  • 使用已经覆盖的当前值作为上方状态,会把不同阶段的数据混在一起。
  • 将上方和左方路径相加,会收集同一条路径无法同时经过的分支,还可能重复计数。
  • 边界以零代替不存在的前驱依赖本题的正价值条件,不能直接照搬到允许负值的同类问题。
  • 行数和列数要分开,一维状态长度对应列数,最终读取的是最后一列。

相似题目

题目 难度 关联与区别
64. 最小路径和 中等 移动方向相同,原题取最小路径和,本题取最大路径和,状态取优方向不同。
1301. 最大得分的路径数目 困难 同样计算网格最优得分,原题还需统计最优路径数量且可对角移动,本题只取最大价值。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2023/87069403
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!