动态规划精进篇:从“二维漫步”到“三向坠落”——下降路径最小和

哈喽,大家好,我是前端小L。

我们的动态规划之旅,已经走过了平坦的网格,也探索了奇特的金字塔。我们发现,无论地形如何变化,DP的核心思想——寻找最优子结构和状态转移——始终是我们手中最锋利的宝剑。

今天,我们将面对一个新的挑战。它既有我们熟悉的 m x n 方格地图,又引入了更自由的移动规则。这就像我们学会了走路和爬山之后,开始尝试“滑雪”——从山顶一路向下,从三条雪道中动态选择最快的那一条!

准备好了吗?让我们一起从山顶“一滑到底”!

力扣 931. 下降路径最小和

https://leetcode.cn/problems/minimum-falling-path-sum/

题目分析: 这次我们不再是从左上角走到右下角了,而是可以从第一行的任意位置出发,目标是到达最后一行的任意位置。每一步,可以从 (i, j) 移动到下一行的 (i+1, j-1), (i+1, j), 或 (i+1, j+1)。目标是找到那条“下降路径”的最小和。

这个移动规则是关键!它和之前只能“向右、向下”的模式完全不同。对于一个点 (i, j),它的“父节点”不再是固定的2个,而是动态的3个。

      (i-1, j-1)  (i-1, j)  (i-1, j+1)
            \       |       /
             \      |      /
              \     |     /
                  (i, j)

思路一:原教旨主义DP,稳扎稳打 (O(m*n) 空间)

面对任何DP问题,我们首先要做的,就是定义清楚 dp 状态。

1. 定义 dp 数组含义: dp[i][j] 表示:从第一行出发,一直下降到 (i, j) 这个位置的最小路径和。

2. 寻找状态转移方程: 要到达 (i, j),我们上一步(在 i-1 行)可能在哪?根据上面的图,有三种可能:

  • 左上方 (i-1, j-1)

  • 正上方 (i-1, j)

  • 右上方 (i-1, j+1)

既然要路径和最小,我们肯定会选择从这三个可能的来源中,路径和最小的那一个转移过来。所以,状态转移方程呼之欲出: dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i-1][j+1]) + matrix[i][j]

3. 处理边界 (这个很重要!): 上面的方程在矩阵中间畅通无阻,但在左右边界呢?

  • j = 0 (第一列):它没有左上方,只有正上方和右上方两个来源。 方程变为 dp[i][0] = min(dp[i-1][0], dp[i-1][1]) + matrix[i][0]

  • j = n-1 (最后一列):它没有右上方,只有正上方和左上方两个来源。 方程变为 dp[i][n-1] = min(dp[i-1][n-1], dp[i-1][n-2]) + matrix[i][n-1]

4. 初始化: 因为我们可以从第一行的任意位置出发,所以 dp 数组的第一行,就等于 matrix 的第一行。dp[0][j] = matrix[0][j]

代码实现 (O(m*n) 空间):

class Solution {
public:
    int minFallingPathSum(vector<vector<int>>& matrix) {
        int n = matrix.size();
        if (n == 0) return 0;
        
        // dp[i][j] 定义为下降到 (i, j) 的最小路径和
        vector<vector<int>> dp(n, vector<int>(n));

        // 初始化第一行
        for (int j = 0; j < n; ++j) {
            dp[0][j] = matrix[0][j];
        }

        // 从第二行开始状态转移
        for (int i = 1; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                int minPrev = dp[i-1][j]; // 正上方
                if (j > 0) { // 如果有左上方
                    minPrev = min(minPrev, dp[i-1][j-1]);
                }
                if (j < n - 1) { // 如果有右上方
                    minPrev = min(minPrev, dp[i-1][j+1]);
                }
                dp[i][j] = minPrev + matrix[i][j];
            }
        }

        // 最终答案在最后一行的所有结果中取最小值
        int result = dp[n-1][0];
        for (int j = 1; j < n; ++j) {
            result = min(result, dp[n-1][j]);
        }
        return result;
    }
};

这个解法思路清晰,稳扎稳打。但 dp 数组占用了 O(n²) 的额外空间,我们能做得更好吗?

思路二:空间优化,高手过招 (O(n) 空间)

是的,你没看错,又是我们熟悉的空间优化环节!

观察状态转移方程,我们发现计算第 i 行时,只依赖于第 i-1 行的数据。这熟悉的配方,意味着我们可以用一个一维数组来解决问题!

让一维数组 dp[n] “滚动”地存储上一行的结果。但这里有一个非常非常重要的陷阱

陷阱分析: 当我们计算dp[j] 时,公式是 min(旧dp[j-1], 旧dp[j], 旧dp[j+1])。 如果我们从左到右 j = 0, 1, 2... 更新 dp 数组,当我们计算 dp[j] 时,dp[j-1] 已经被本轮循环更新成新值了!但我们此刻需要的是上一轮旧值 dp[j-1]。怎么办?

解决方案:请个外援! 在开始一行的计算时,我们用一个临时变量 prev (或者叫 topLeft) 来保存上一轮的 dp[j-1] 的值。

优化后的流程: 遍历第 i 行时,我们内层循环遍历 j = 0 to n-1

  1. 用一个 temp 变量保存当前 dp[j]旧值

  2. 计算 dp[j]。此时它需要的“左上角”的值,就是我们之前保存的 prev。它需要的“正上方”和“右上方”的值,分别是旧的 dp[j]dp[j+1],它们还没被更新,可以直接用。

  3. 更新完 dp[j] 后,把刚才保存的 temp (也就是旧 dp[j] 的值) 赋给 prev,供下一次 j+1 的循环使用。

最终代码实现 (O(n) 空间):

class Solution {
public:
    int minFallingPathSum(vector<vector<int>>& matrix) {
        int n = matrix.size();
        if (n == 0) return 0;

        // dp数组只用一行大小,初始化为矩阵的第一行
        vector<int> dp = matrix[0];

        // 从第二行开始状态转移
        for (int i = 1; i < n; ++i) {
            // prev 用于保存上一行的 dp[j-1],也就是左上角的值
            int prev = dp[0]; 
            
            // 单独处理第一个元素
            dp[0] = min(dp[0], dp[1]) + matrix[i][0];

            for (int j = 1; j < n - 1; ++j) {
                int temp = dp[j]; // 保存当前位置的旧值 (即上一行的 dp[j])
                dp[j] = min({prev, dp[j], dp[j+1]}) + matrix[i][j];
                prev = temp; // 更新 prev,供下一次循环使用
            }

            // 单独处理最后一个元素
            dp[n-1] = min(dp[n-1], prev) + matrix[i][n-1];
        }

        // 最终答案在dp数组中取最小值
        int result = dp[0];
        for (int j = 1; j < n; ++j) {
            result = min(result, dp[j]);
        }
        return result;
    }
};

(注:为了逻辑更清晰,我将边界和中间分开处理。也可以在循环中用更多变量处理,但思路是相同的)

总结:提炼思维模型

到目前为止,我们解决的这几道题,其实代表了网格/矩阵类DP的两种基础模型:

  1. 固定双向选择模型:如“不同路径”、“最小路径和”,每个点的来源固定来自上方和左方。状态转移方程非常规整。

  2. 多向动态选择模型:如“三角形”、“下降路径和”,每个点的来源有多个,且在边界处会发生变化。这类问题更能考验我们对边界条件的处理,也是空间优化的难点和亮点。

从简单的二维漫步,到考虑障碍,再到金字塔寻宝,直到今天的三向坠落,我们对动态规划的理解正在从“点”连成“线”,从“线”织成“面”。我们不再是死记硬背模板,而是真正理解了如何根据题目的约束,去定义状态推导转移处理边界,并最终寻求优化

这,就是高质量学习的魅力!

如果这篇深度剖析对你有所启发,请不要吝啬你的点赞和收藏,这是对我最大的鼓励!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值