【经典算法】LeetCode 64. 最小路径和(Java/C/Python3/Golang实现含注释说明,Easy)

简介: 【经典算法】LeetCode 64. 最小路径和(Java/C/Python3/Golang实现含注释说明,Easy)

题目描述

给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,
使得路径上的数字总和为最小。

说明:每次只能向下或者向右移动一步。
 
示例 1:
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。
示例 2:
输入:grid = [[1,2,3],[4,5,6]]
输出:12
 
提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 200
0 <= grid[i][j] <= 200

原题:LeetCode 64.最小路径和

思路及实现

方式一:动态规划(朴素DP )

思路

可以使用动态规划来求解最小路径和。

要求从左上角转移到右下角的最小路径和,每次只能往右走或者往下走。换句话说,对于位置 (i, j),其只能从 (i-1, j) 和 (i, j-1) 两个位置转移而来。我们设 dp[i][j] 表示到达位置 (i, j) 的最小路径和。

那么 dp[i][j] 肯定要从 dp[i-1][j] 和 dp[i][j-1] 中那个更小的状态转移过来,

因此状态转移方程为:dp[i][j] = min(dp[i][j-1], dp[i-1][j]) + grid[i][j]

边界条件:

  • 当 i = 0, j = 0 时,即左上角位置,其左侧和上侧单元格都不存在,dp[0][0] = grid[0][0]
  • 当 i = 0, j ≠ 0 时,即首行元素,其上侧单元格不存在,dp[0][j] = dp[0][j-1] + grid[0][j];
  • 当 i ≠ 0, j = 0 时,即首列元素,其左侧单元格不存在,dp[i][0] = dp[i-1][0] + grid[i][0];

代码实现

Java版本
public 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];
        }
        // 填充其他位置
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
            }
        }
        // 最后一个元素即结果
        return dp[m - 1][n - 1];
    }
}

说明:

此解法使用动态规划,时间复杂度为 O(mn),空间复杂度也为 O(mn)。

C语言版本
#include <stdio.h>
#include <limits.h>
int minPathSum(int** grid, int gridSize, int* gridColSize){
    if (grid == NULL || gridSize == 0 || gridColSize[0] == 0) {
        return 0;
    }
    int m = gridSize;
    int n = gridColSize[0];
    int** dp = (int**)malloc(m * sizeof(int*));
    for (int i = 0; i < m; i++) {
        dp[i] = (int*)malloc(n * sizeof(int));
    }
    // 初始化第一行和第一列
    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];
    }
    // 填充其他位置
    for (int i = 1; i < m; i++) {
        for (int j = 1; j < n; j++) {
            dp[i][j] = (dp[i - 1][j] < dp[i][j - 1]) ? (dp[i - 1][j] + grid[i][j]) : (dp[i][j - 1] + grid[i][j]);
        }
    }
    int result = dp[m - 1][n - 1];
    // 释放动态分配的内存
    for (int i = 00; i < m; i++) {
        free(dp[i]);
    }
    free(dp);
    return result;
}
int main() {
    // 示例网格
    int gridSize = 3;
    int gridColSize[3] = {3, 3, 3};
    int grid[3][3] = {{1, 3, 1}, {1, 5, 1}, {4, 2, 1}};
    int** gridPtr = (int**)malloc(gridSize * sizeof(int*));
    for (int i = 0; i < gridSize; i++) {
        gridPtr[i] = grid[i];
    }
    int minSum = minPathSum(gridPtr, gridSize, gridColSize);
    printf("The minimum path sum is: %d\n", minSum);
    // 释放动态分配的内存
    free(gridPtr);
    return 0;
}

说明:

C语言版本同样使用动态规划,时间复杂度和空间复杂度与Java版本相同。注意在C语言中需要手动管理内存,因此在函数结束时需要释放动态分配的内存。

Python3版本
def minPathSum(grid):
    if not grid or not grid[0]:
        return 0
    m, n = len(grid), len(grid[0])
    dp = [[0] * n for _ in range(m)]
    # 初始化第一行和第一列
    dp[0][0] = grid[0][0]
    for i in range(1, m):
        dp[i][0] = dp[i - 1][0] + grid[i][0]
    for j in range(1, n):
        dp[0][j] = dp[0][j - 1] + grid[0][j]
    # 填充其他位置
    for i in range(1, m):
        for j in range(1, n):
            dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]
    return dp[m - 1][n - 1]
# test
# 示例网格
grid = [
    [1, 3, 1],
    [1, 5, 1],
    [4, 2, 1]
]
min_sum = minPathSum(grid)
print(f"The minimum path sum is: {min_sum}")

说明:

Python3版本使用动态规划,简洁易懂。由于Python中的列表支持动态大小,因此不需要手动管理内存。

go
// minPathSum 使用朴素DP方法求解最小路径和
func minPathSum(grid [][]int) int {
    if len(grid) == 0 || len(grid[0]) == 0 {
        return 0
    }
    rows, cols := len(grid), len(grid[0])
    
    // 创建一个二维DP数组,大小与grid相同,用于存储到达每个位置的最小路径和
    dp := make([][]int, rows)
    for i := range dp {
        dp[i] = make([]int, cols)
    }
    
    // 初始化第一行和第一列
    dp[0][0] = grid[0][0]
    for i := 1; i < rows; i++ {
        dp[i][0] = dp[i-1][0] + grid[i][0] // 第一列的最小路径和依赖于上一行的值
    }
    for j := 1; j < cols; j++ {
        dp[0][j] = dp[0][j-1] + grid[0][j] // 第一行的最小路径和依赖于前一列的值
    }
    
    // 填充剩余的DP数组
    for i := 1; i < rows; i++ {
        for j := 1; j < cols; j++ {
            // 当前位置的最小路径和取上方和左方位置的最小路径和加上当前位置的值
            dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
        }
    }
    
    // 返回右下角位置的最小路径和
    return dp[rows-1][cols-1]
}
// min 返回两个整数中的较小值
func min(a, b int) int {
    if a < b {
        return a
    }
    return b
}
// 示例使用
func main() {
    grid := [][]int{
        {1, 3, 1},
        {1, 5, 1},
        {4, 2, 1},
    }
    minSum := minPathSum(grid)
    fmt.Printf("The minimum path sum is: %d\n", minSum)
}

说明:

minPathSum:使用朴素DP算法计算从grid左上角到右下角的最小路径和。

逻辑步骤

  1. 初始化二维DP数组dp,大小与grid相同。
  2. 填充dp数组的第一行和第一列。
  3. 遍历grid剩余元素,根据状态转移方程填充dp数组。
  4. 返回dp数组的右下角元素作为结果。

复杂度分析

  • 时间复杂度:O(mn),其中 m 和 n 分别是网格的行数和列数。需要遍历每个网格位置一次。
  • 空间复杂度:O(mn),需要一个二维数组 dp 来存储每个位置的最小路径和。

方式二:滚动数组优化DP

对于上述的动态规划解法,我们注意到在计算 dp[i][j] 时,只依赖于其正上方和左方的值。因此,我们可以使用滚动数组来优化空间复杂度,将空间复杂度从 O(mn) 降低到 O(n)。

思路

由于每一行的计算只依赖于上一行的值,我们可以只保留上一行的值,并在当前行上直接计算。这样,我们只需要一个一维数组来存储上一行的值即可。

代码实现

Java版本
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[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++) {
        int prev = dp[0]; // 保存上一行的第一个值,因为下一个dp[0]会被覆盖
        for (int j = 0; j < n; j++) {
            int curr = dp[j]; // 保存当前位置的值,因为下一个dp[j]会被覆盖
            if (j == 0) {
                // 如果是第一列,则只能从上方来
                dp[j] = prev + grid[i][j];
            } else {
                // 否则取上方和左方的最小值
                dp[j] = Math.min(prev, dp[j]) + grid[i][j];
            }
            prev = curr; // 更新prev为下一个位置的上一行值
        }
    }
    // dp数组的最后一个元素即为最小路径和
    return dp[n - 1];
}
C语言版本
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int minPathSum(int** grid, int gridSize, int* gridColSize) {
    if (grid == NULL || gridSize == 0 || gridColSize[0] == 0) {
        return 0;
    }
    int n = gridColSize[0];
    int* dp = (int*)malloc(n * sizeof(int));
    if (dp == NULL) {
        return -1;
    }
    // 初始化第一行
    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 < gridSize; i++) {
        int prev = dp[0]; // 保存上一行的第一个值
        for (int j = 0; j < n; j++) {
            int temp = dp[j]; // 临时保存当前位置的值
            if (j == 0) {
                dp[j] = prev + grid[i][j];
            } else {
                dp[j] = (prev < dp[j]) ? (prev + grid[i][j]) : (dp[j] + grid[i][j]);
            }
            prev = temp; // 更新prev
        }
    }
    int result = dp[n - 1];
    free(dp);
    return result;
}
int main() {
    int gridSize = 3;
    int gridColSize[3] = {3, 3, 3};
    int grid[3][3] = {{1, 3, 1}, {1, 5, 1}, {4, 2, 1}};
    int** gridPtr = (int**)malloc(gridSize * sizeof(int*));
    for (int i = 0; i < gridSize; i++) {
        gridPtr[i] = (int*)malloc(gridColSize[i] * sizeof(int));
        for (int j = 0; j < gridColSize[i]; j++) {
            gridPtr[i][j] = grid[i][j];
        }
    }
    int minSum = minPathSum(gridPtr, gridSize, gridColSize);
    printf("The minimum path sum is: %d\n", minSum);
    // 释放动态分配的内存
    for (int i = 0; i < gridSize; i++) {
        free(gridPtr[i]);
    }
    free(gridPtr);
    return 0;
}
Python3版本
def minPathSum(grid):
    if not grid or not grid[0]:
        return 0
    m, n = len(grid), len(grid[0])
    dp = [0] * n
    # 初始化第一行
    dp[0] = grid[0][0]
    for j in range(1, n):
        dp[j] = dp[j - 1] + grid[0][j]
    # 填充其他行
    for i in range(1, m):
        prev = dp[0]
        for j in range(n):
            curr = dp[j]
            dp[j] = prev + grid[i][j] if j == 0 else min(prev, dp[j]) + grid[i][j]
            prev = curr
    return dp[n - 1]
# 示例网格
grid = [
    [1, 3, 1],[1, 5, 1],
    [4, 2, 1]
]
min_sum = minPathSum(grid)
print(f"The minimum path sum is: {min_sum}")
go
// minPathSumOpt 使用滚动数组优化DP方法求解最小路径和
func minPathSumOpt(grid [][]int) int {
    if len(grid) == 0 || len(grid[0]) == 0 {
        return 0
    }
    rows, cols := len(grid), len(grid[0])
    
    // 只需要一个一维数组来保存当前行的最小路径和
    prevRow := make([]int, cols)
    currRow := make([]int, cols)
    
    // 初始化第一列
    prevRow[0] = grid[0][0]
    for j := 1; j < cols; j++ {
        prevRow[j] = prevRow[j-1] + grid[0][j]
    }
    
    // 填充剩余的行
    for i := 1; i < rows; i++ {
        // 交换prevRow和currRow,currRow用于保存当前行的最小路径和
        prevRow, currRow = currRow, prevRow
        
        // 初始化当前行的第一列
        currRow[0] = prevRow[0] + grid[i][0]
        
        // 填充当前行的剩余列
        for j := 1; j < cols; j++ {
            // 当前位置的最小路径和取上方和左方位置的最小路径和加上当前位置的值
            currRow[j] = min(prevRow[j], currRow[j-1]) + grid[i][j]
        }
    }
    
    // 返回最后一行的最后一个元素,即最小路径和
    return currRow[cols-1]
}
// min 返回两个整数中的较小值
func min(a, b int) int {
    if a < b {
        return a
    }
    return b
}
// 示例使用
func main() {
    grid := [][]int{
        {1, 3, 1},
        {1, 5, 1},
        {4, 2, 1},
}
minSum := minPathSumOpt(grid)
fmt.Printf("The minimum path sum with rolling array optimization is: %d\n", minSum)
}

说明:

函数功能

minPathSumOpt:使用滚动数组优化DP算法计算从grid左上角到右下角的最小路径和。

步奏

  1. 初始化两个一维数组prevRowcurrRow
  2. 填充prevRow数组作为第一行的最小路径和。
  3. 遍历grid剩余行,交替使用prevRowcurrRow计算最小路径和。
  4. 返回currRow的最后一个元素作为结果。

复杂度分析

  • 时间复杂度:O(mn),其中 m 和 n 分别是网格的行数和列数。仍然需要遍历每个网格位置一次。
  • 空间复杂度:O(n),其中 n 是网格的列数。使用滚动数组只需要存储一行的值。

滚动数组优化后的空间复杂度更低,但时间复杂度保持不变。这种优化在处理大规模网格时尤其有用,因为它显著减少了内存的使用。

总结

解法 思路 特点 优点 缺点 时间复杂度 空间复杂度
朴素DP 填充DP表,状态转移 每个位置的状态取决于上方和左方 直观易懂 空间占用大 O(mn) O(mn)
滚动数组优化DP 只用一维数组,通过循环更新 节省空间,利用上一行的值 节省空间 编程稍微复杂 O(mn) O(n)

相似题目

相似题目 难度 链接
leetcode 349 两个数组的交集 简单 力扣-349
leetcode 62 不同路径 中等 力扣-62
leetcode 63 不同路径 II 中等 力扣-63
leetcode 64 最小路径和 中等 力扣-64
leetcode 120 三角形最小路径和 中等 力扣-120
leetcode 152 乘积最大子数组 中等 力扣-152
leetcode 198 打家劫舍 中等 力扣-198
leetcode 213 打家劫舍 II 困难 力扣-213
leetcode 300 最长递增子序列 中等 力扣-300
leetcode 309 最佳买卖股票时机含冷冻期 中等 力扣-309

这些题目都涉及到动态规划的思想,有些题目可能需要使用滚动数组进行优化,有些题目则需要根据特定条件调整状态转移方程。通过练习这些题目,可以加深对动态规划的理解和掌握。

相关文章
|
12天前
|
机器学习/深度学习 人工智能 算法
基于Python深度学习的眼疾识别系统实现~人工智能+卷积网络算法
眼疾识别系统,本系统使用Python作为主要开发语言,基于TensorFlow搭建卷积神经网络算法,并收集了4种常见的眼疾图像数据集(白内障、糖尿病性视网膜病变、青光眼和正常眼睛) 再使用通过搭建的算法模型对数据集进行训练得到一个识别精度较高的模型,然后保存为为本地h5格式文件。最后使用Django框架搭建了一个Web网页平台可视化操作界面,实现用户上传一张眼疾图片识别其名称。
61 4
基于Python深度学习的眼疾识别系统实现~人工智能+卷积网络算法
|
1月前
|
机器学习/深度学习 人工智能 算法
猫狗宠物识别系统Python+TensorFlow+人工智能+深度学习+卷积网络算法
宠物识别系统使用Python和TensorFlow搭建卷积神经网络,基于37种常见猫狗数据集训练高精度模型,并保存为h5格式。通过Django框架搭建Web平台,用户上传宠物图片即可识别其名称,提供便捷的宠物识别服务。
283 55
|
22天前
|
存储 缓存 监控
局域网屏幕监控系统中的Python数据结构与算法实现
局域网屏幕监控系统用于实时捕获和监控局域网内多台设备的屏幕内容。本文介绍了一种基于Python双端队列(Deque)实现的滑动窗口数据缓存机制,以处理连续的屏幕帧数据流。通过固定长度的窗口,高效增删数据,确保低延迟显示和存储。该算法适用于数据压缩、异常检测等场景,保证系统在高负载下稳定运行。 本文转载自:https://www.vipshare.com
117 66
|
2月前
|
搜索推荐 Python
利用Python内置函数实现的冒泡排序算法
在上述代码中,`bubble_sort` 函数接受一个列表 `arr` 作为输入。通过两层循环,外层循环控制排序的轮数,内层循环用于比较相邻的元素并进行交换。如果前一个元素大于后一个元素,就将它们交换位置。
147 67
|
2月前
|
存储 搜索推荐 Python
用 Python 实现快速排序算法。
快速排序的平均时间复杂度为$O(nlogn)$,空间复杂度为$O(logn)$。它在大多数情况下表现良好,但在某些特殊情况下可能会退化为最坏情况,时间复杂度为$O(n^2)$。你可以根据实际需求对代码进行调整和修改,或者尝试使用其他优化策略来提高快速排序的性能
139 61
|
2月前
|
算法 数据安全/隐私保护 开发者
马特赛特旋转算法:Python的随机模块背后的力量
马特赛特旋转算法是Python `random`模块的核心,由松本真和西村拓士于1997年提出。它基于线性反馈移位寄存器,具有超长周期和高维均匀性,适用于模拟、密码学等领域。Python中通过设置种子值初始化状态数组,经状态更新和输出提取生成随机数,代码简单高效。
129 63
|
1月前
|
机器学习/深度学习 人工智能 算法
【宠物识别系统】Python+卷积神经网络算法+深度学习+人工智能+TensorFlow+图像识别
宠物识别系统,本系统使用Python作为主要开发语言,基于TensorFlow搭建卷积神经网络算法,并收集了37种常见的猫狗宠物种类数据集【'阿比西尼亚猫(Abyssinian)', '孟加拉猫(Bengal)', '暹罗猫(Birman)', '孟买猫(Bombay)', '英国短毛猫(British Shorthair)', '埃及猫(Egyptian Mau)', '缅因猫(Maine Coon)', '波斯猫(Persian)', '布偶猫(Ragdoll)', '俄罗斯蓝猫(Russian Blue)', '暹罗猫(Siamese)', '斯芬克斯猫(Sphynx)', '美国斗牛犬
188 29
【宠物识别系统】Python+卷积神经网络算法+深度学习+人工智能+TensorFlow+图像识别
|
19天前
|
算法 网络协议 Python
探秘Win11共享文件夹之Python网络通信算法实现
本文探讨了Win11共享文件夹背后的网络通信算法,重点介绍基于TCP的文件传输机制,并提供Python代码示例。Win11共享文件夹利用SMB协议实现局域网内的文件共享,通过TCP协议确保文件传输的完整性和可靠性。服务器端监听客户端连接请求,接收文件请求并分块发送文件内容;客户端则连接服务器、接收数据并保存为本地文件。文中通过Python代码详细展示了这一过程,帮助读者理解并优化文件共享系统。
|
24天前
|
存储 算法 Python
文件管理系统中基于 Python 语言的二叉树查找算法探秘
在数字化时代,文件管理系统至关重要。本文探讨了二叉树查找算法在文件管理中的应用,并通过Python代码展示了其实现过程。二叉树是一种非线性数据结构,每个节点最多有两个子节点。通过文件名的字典序构建和查找二叉树,能高效地管理和检索文件。相较于顺序查找,二叉树查找每次比较可排除一半子树,极大提升了查找效率,尤其适用于海量文件管理。Python代码示例包括定义节点类、插入和查找函数,展示了如何快速定位目标文件。二叉树查找算法为文件管理系统的优化提供了有效途径。
50 5
|
24天前
|
存储 缓存 算法
探索企业文件管理软件:Python中的哈希表算法应用
企业文件管理软件依赖哈希表实现高效的数据管理和安全保障。哈希表通过键值映射,提供平均O(1)时间复杂度的快速访问,适用于海量文件处理。在Python中,字典类型基于哈希表实现,可用于管理文件元数据、缓存机制、版本控制及快速搜索等功能,极大提升工作效率和数据安全性。
61 0

热门文章

最新文章