Skip to content

最小路径和:动态规划的网格探险

大家好!继续我们的二维动态规划探索之旅,今天要一起攻克的是LeetCode上另一道经典题目:最小路径和。这道题是"不同路径"的近亲,同样考察我们在网格上的动态规划思维,但有了一个新的挑战——我们不仅要到达终点,还要寻找代价最小的那条路径。让我们一起深入这个问题!

🌟 问题的本质:从成本到最优解

在"不同路径"中,我们关注的是路径的数量;而在"最小路径和"中,我们关注的是路径的质量。这反映了动态规划的两大核心问题类型:

  • 计数问题:有多少种方法/路径?
  • 最优化问题:最大/最小值是多少?

这种转变也反映了我们在实际生活中常面临的问题:不仅要知道有多少种选择,更要找出最佳选择。

🎯 问题描述:带权重的网格漫步

给定一个包含非负整数的 m x n 网格 grid,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。 说明:每次只能向下或者向右移动一步。

例如:

输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:路径 1→3→1→1→1 的总和最小,为 7。

🤔 从小例子开始思考

让我们从简单的例子开始:

2x2网格:
[1, 3]
[2, 1]

可能的路径:
1. 右→下:1 + 3 + 1 = 5
2. 下→右:1 + 2 + 1 = 4

最小路径和为4

再看一个稍复杂的例子:

3x3网格:
[1, 3, 1]
[1, 5, 1]
[4, 2, 1]

可能的路径有多条,让我们追踪其中几条:
1. 右→右→下→下:1 + 3 + 1 + 1 + 1 = 7
2. 右→下→下→右:1 + 3 + 5 + 2 + 1 = 12
3. 下→右→右→下:1 + 1 + 5 + 1 + 1 = 9
4. 下→下→右→右:1 + 1 + 4 + 2 + 1 = 9

最小路径和为7

💡 发现规律:最优子结构

通过分析,我们发现一个关键洞察:

要到达位置(i,j),并且总和最小,只能通过:

  • 从上方(i-1,j)到达,且上方已经是到达该点的最小路径和
  • 从左方(i,j-1)到达,且左方已经是到达该点的最小路径和

我们选择其中较小的那个,再加上当前格子的值。

📝 二维动态规划解法

状态定义:

  • dp[i][j] 表示:从起点(0,0)到位置(i,j)的最小路径和

状态转移方程:

  • dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1])
java
class Solution {
    public int minPathSum(int[][] grid) {
        if (grid == null || grid.length == 0 || grid[0].length == 0) {
            return 0;
        }
        
        int m = grid.length;
        int n = grid[0].length;
        int[][] dp = new int[m][n];
        
        // 初始化起点
        dp[0][0] = grid[0][0];
        
        // 初始化第一列
        for (int i = 1; i < m; i++) {
            dp[i][0] = dp[i-1][0] + grid[i][0];
        }
        
        // 初始化第一行
        for (int j = 1; j < n; j++) {
            dp[0][j] = dp[0][j-1] + grid[0][j];
        }
        
        // 填充dp数组
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                dp[i][j] = grid[i][j] + Math.min(dp[i-1][j], dp[i][j-1]);
            }
        }
        
        return dp[m-1][n-1];
    }
}

🎨 图解DP的执行过程

以3x3网格为例:

原始网格:
[1, 3, 1]
[1, 5, 1]
[4, 2, 1]

1. 初始化起点:
   dp[0][0] = 1

2. 初始化第一行和第一列:
   [1, 4, 5]
   [2, □, □]
   [6, □, □]

3. 填充dp[1][1]:
   [1, 4, 5]
   [2, 7, □]
   [6, □, □]
   解释:取min(2,4)+5=7

4. 填充dp[1][2]:
   [1, 4, 5]
   [2, 7, 8]
   [6, □, □]
   解释:取min(7,5)+1=6

... 依此类推

最终得到dp数组:
   [1, 4, 5]
   [2, 7, 6]
   [6, 8, 7]

所以最小路径和是7。

📊 执行轨迹分析

当我们填充dp数组时,每个位置的值是如何决定的?让我们看看dp[2][1]这个位置:

可以从dp[1][1](值为7)向下走,总和为7+2=9 也可以从dp[2][0](值为6)向右走,总和为6+2=8

因为8<9,所以我们选择从左边过来,得到dp[2][1]=8。

这种"比较+选择"的过程正是动态规划解决最优化问题的核心。

💫 空间优化:降维处理

与"不同路径"类似,我们观察到每个位置只依赖其上方和左方的值,因此可以使用一维数组优化空间:

java
class Solution {
    public int minPathSum(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        int[] dp = new int[n];
        
        dp[0] = grid[0][0];
        for (int j = 1; j < n; j++) {
            dp[j] = dp[j-1] + grid[0][j];
        }
        
        for (int i = 1; i < m; i++) {
            dp[0] += grid[i][0];
            for (int j = 1; j < n; j++) {
                dp[j] = Math.min(dp[j], dp[j-1]) + grid[i][j];
            }
        }
        
        return dp[n-1];
    }
}

优化后,空间复杂度从O(m×n)降至O(n),这在面对大型网格时尤为重要。

🔍 最优路径的追踪

有时我们不仅需要知道最小路径和,还需要知道具体的路径。可以通过额外存储每个位置的选择来实现:

java
class Solution {
    public List<int[]> getMinPath(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        int[][] dp = new int[m][n];
        int[][] choices = new int[m][n]; // 0表示从上方来,1表示从左方来
        
        // 初始化和填充dp与choices数组
        // ...省略类似前面的初始化代码...
        
        // 从终点回溯
        List<int[]> path = new ArrayList<>();
        int i = m-1, j = n-1;
        path.add(new int[]{i, j});
        
        while (i > 0 || j > 0) {
            if (choices[i][j] == 0) {
                i--;
            } else {
                j--;
            }
            path.add(0, new int[]{i, j});
        }
        
        return path;
    }
}

🎭 变体问题:不同约束下的最小路径和

实际应用中,我们可能会遇到各种变体:

  1. 有障碍物:某些格子无法通过,需要绕行
  2. 对角线移动:除了向右和向下,还可以斜向移动
  3. 负权重:格子值可能为负,需要避免环路

每种变体都会改变我们的状态转移方程,但核心思想仍然是:通过已知的最优子结构,构建当前位置的最优解。

💡 解题技巧与总结

  1. 边界处理:二维DP中,边界初始化非常重要
  2. 状态压缩:当依赖关系简单时,可以压缩空间
  3. 路径追踪:需要记录决策过程,以便回溯
  4. 问题建模:将复杂问题转化为网格路径问题

🎓 面试指南

面试官常常会在此类问题上加入变体,如何应对?

  1. 先解决基础问题,明确DP状态和转移方程
  2. 分析变体带来的新约束,相应调整转移方程
  3. 考虑时间和空间复杂度,是否可以优化
  4. 实现代码时注意边界情况处理

🔗 相似题目与延伸

  • 三角形最小路径和
  • 最大正方形
  • 矩阵中的最长递增路径

🤔 思考题

如果网格中允许向左和向上移动(但不允许形成环),如何修改算法?


作者:忍者算法
公众号:忍者算法

二维动态规划不仅是算法面试的高频考点,也是解决实际路径规划问题的重要工具。通过掌握"最小路径和"这类问题,我们不仅强化了DP的基本思路,也培养了解决更复杂优化问题的能力。希望这篇文章能帮助你在算法之路上更进一步!

#算法面试 #LeetCode #动态规划 #最优化问题 #网格问题