LeetCode 64:最小路径和复盘|二维 DP 与 ACM 模式完整写法

一、题目

给定一个包含非负整数的 m × n 网格:

复制代码
grid

要求从:

复制代码
左上角

走到:

复制代码
右下角

每次只能:

复制代码
向右
或者
向下

求路径上的:

复制代码
最小数字总和

例如:

复制代码
grid =

1 3 1
1 5 1
4 2 1

最优路径:

复制代码
1 → 3 → 1
        ↓
        1
        ↓
        1

路径和:

复制代码
1 + 3 + 1 + 1 + 1 = 7

所以答案:

复制代码
7

二、这道题考什么?

这是一道非常典型的:

复制代码
二维动态规划 DP

关键在于:

到达当前格子 (i,j) 时,只可能从上面或者左边过来。

也就是:

复制代码
(i-1,j)

或者:

复制代码
(i,j-1)

所以只需要比较:

复制代码
上面的最小路径和
左边的最小路径和

取更小的一个。


三、DP 状态怎么定义?

定义:

复制代码
dp[i][j]

表示:

从左上角走到 (i,j) 的最小路径和。

例如:

复制代码
dp[2][1]

表示:

从左上角走到第 2 行第 1 列这个位置时,最少需要多少路径和。


四、状态转移公式

因为 (i,j) 只能从:

复制代码
上:
dp[i-1][j]

左:
dp[i][j-1]

过来。

所以:

复制代码
dp[i][j]
=
Math.min(dp[i - 1][j], dp[i][j - 1])
+ grid[i][j];

翻译成人话:

到当前格子的最小路径和 = 上面和左边选一个更小的 + 当前格子的值。

这就是整道题最核心的一句。


五、为什么第一行和第一列要单独处理?

普通位置:

复制代码
(i,j)

有两个来源:

复制代码
上
左

但是第一行:

复制代码
i = 0

上面已经没有格子。

所以第一行只能:

复制代码
一直从左边走过来

例如:

复制代码
1 3 1 2

那么:

复制代码
dp[0][0] = 1
dp[0][1] = 1 + 3
dp[0][2] = 1 + 3 + 1
...

代码:

复制代码
for (int j = 1; j < n; j++) {
    dp[0][j] = dp[0][j - 1] + grid[0][j];
}

第一列也是一样。

第一列没有左边:

复制代码
j = 0

所以只能:

复制代码
从上往下

代码:

复制代码
for (int i = 1; i < m; i++) {
    dp[i][0] = dp[i - 1][0] + grid[i][0];
}

六、起点怎么初始化?

左上角:

复制代码
(0,0)

就是起点。

所以:

复制代码
dp[0][0] = grid[0][0];

因为:

到起点本身的路径和,就是起点自己的值。


七、LeetCode 标准写法

复制代码
class Solution {
    public int minPathSum(int[][] grid) {

        int m = grid.length;
        int n = grid[0].length;

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

        dp[0][0] = grid[0][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++) {
            dp[i][0] = dp[i - 1][0] + grid[i][0];
        }

        // 其他格子
        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];
    }
}

八、ACM 模式和 LeetCode 有什么区别?

LeetCode 会帮我们自动准备:

复制代码
int[][] grid

我们只需要写算法。

但是 ACM 模式需要自己负责:

复制代码
读取输入
↓
构造二维数组
↓
调用算法
↓
输出答案

所以 ACM 一般写:

复制代码
public class Main

并且有:

复制代码
public static void main(String[] args)

九、假设 ACM 输入格式

例如输入:

复制代码
3 3
1 3 1
1 5 1
4 2 1

第一行:

复制代码
3 3

表示:

复制代码
m = 3
n = 3

也就是:

复制代码
3 行
3 列

后面:

复制代码
1 3 1
1 5 1
4 2 1

就是二维数组。


十、ACM 完整版本

复制代码
import java.util.*;

public class Main {

    public static void main(String[] args) {

        Scanner in = new Scanner(System.in);

        // 读取行数和列数
        int m = in.nextInt();
        int n = in.nextInt();

        // 创建二维数组
        int[][] grid = new int[m][n];

        // 读取矩阵
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                grid[i][j] = in.nextInt();
            }
        }

        // 调用算法
        int ans = minPathSum(grid);

        // 输出答案
        System.out.println(ans);
    }

    private static int minPathSum(int[][] grid) {

        int m = grid.length;
        int n = grid[0].length;

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

        dp[0][0] = grid[0][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++) {
            dp[i][0] = dp[i - 1][0] + grid[i][0];
        }

        // 其他格子:从上面和左边选更小的
        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];
    }
}

十一、ACM 二维数组输入模板

这一段建议直接背:

复制代码
int m = in.nextInt();
int n = in.nextInt();

int[][] grid = new int[m][n];

for (int i = 0; i < m; i++) {
    for (int j = 0; j < n; j++) {
        grid[i][j] = in.nextInt();
    }
}

记住:

复制代码
i → 行
j → 列

所以:

复制代码
grid[i][j]

表示:

第 i 行、第 j 列。


十二、为什么 main 里面调用的方法写 static?

因为:

复制代码
public static void main(...)

本身是一个静态方法。

如果想直接:

复制代码
int ans = minPathSum(grid);

那么:

复制代码
minPathSum

也最好写成:

复制代码
private static int minPathSum(...)

否则需要:

复制代码
Main obj = new Main();
obj.minPathSum(grid);

面试没必要这么麻烦。

所以 ACM 模式里可以统一:

复制代码
main 是 static
辅助算法也写 static

十三、完整运行过程

输入:

复制代码
3 3
1 3 1
1 5 1
4 2 1

首先:

复制代码
m = 3
n = 3

构造:

复制代码
grid =

1 3 1
1 5 1
4 2 1

初始化起点

复制代码
dp[0][0] = 1

初始化第一行

复制代码
dp[0][1]
=
dp[0][0] + grid[0][1]
=
1 + 3
=
4

dp[0][2]
=
dp[0][1] + grid[0][2]
=
4 + 1
=
5

第一行:

复制代码
1 4 5

初始化第一列

复制代码
dp[1][0]
=
1 + 1
=
2

dp[2][0]
=
2 + 4
=
6

于是:

复制代码
dp =

1 4 5
2 ? ?
6 ? ?

十四、计算其他格子

计算:

复制代码
dp[1][1]

上面:

复制代码
dp[0][1] = 4

左边:

复制代码
dp[1][0] = 2

选更小:

复制代码
2

再加当前:

复制代码
grid[1][1] = 5

所以:

复制代码
dp[1][1] = 7

计算:

复制代码
dp[1][2]

上面:

复制代码
5

左边:

复制代码
7

取:

复制代码
5

加当前:

复制代码
1

得到:

复制代码
6

继续计算,最终:

复制代码
dp =

1 4 5
2 7 6
6 8 7

所以:

复制代码
dp[2][2] = 7

答案:

复制代码
7

十五、为什么最终返回 dpm-1n-1

因为数组下标从:

复制代码
0

开始。

如果:

复制代码
m 行
n 列

最后一个位置就是:

复制代码
(m-1,n-1)

所以:

复制代码
return dp[m - 1][n - 1];

就是右下角的最小路径和。


十六、复杂度分析

每个格子只计算一次。

一共有:

复制代码
m × n

个格子。

所以时间复杂度:

复制代码
O(mn)

我们又创建了:

复制代码
m × n

的 dp 数组。

所以空间复杂度:

复制代码
O(mn)

十七、这题怎么快速想到 DP?

看到题目:

复制代码
从左上角走到右下角
只能向右或向下
求最小路径和

可以立刻问自己:

到当前格子之前,我可能从哪里来?

答案:

复制代码
上面
左边

于是:

复制代码
当前最优
=
min(上面的最优, 左边的最优)
+
当前值

这就是 DP。


十八、和零钱兑换有什么共同点?

零钱兑换:

复制代码
dp[i]
=
凑 i 元最少需要几个硬币

当前状态来源:

复制代码
dp[i-coin]

本题:

复制代码
dp[i][j]
=
到达 (i,j) 的最小路径和

当前状态来源:

复制代码
dp[i-1][j]
dp[i][j-1]

所以 DP 的通用思路是:

复制代码
1. dp 表示什么?

2. 当前状态可以从哪些旧状态过来?

3. 初始状态是什么?

4. 最终答案在哪?

十九、最容易写错的地方

1. m 和 n 搞反

复制代码
int m = grid.length;
int n = grid[0].length;

记:

复制代码
grid.length
→ 行数

grid[0].length
→ 列数

2. 第一行忘记初始化

第一行不能直接使用:

复制代码
dp[i - 1][j]

因为:

复制代码
i = 0

会出现:

复制代码
dp[-1][j]

所以必须单独处理。


3. 第一列忘记初始化

第一列:

复制代码
j = 0

没有左边。

必须:

复制代码
dp[i][0] = dp[i - 1][0] + grid[i][0];

4. 状态转移忘记加当前值

错误:

复制代码
dp[i][j] =
Math.min(dp[i - 1][j], dp[i][j - 1]);

正确:

复制代码
dp[i][j] =
Math.min(dp[i - 1][j], dp[i][j - 1])
+ grid[i][j];

因为路径经过当前格子,它自己的值也必须算进去。


5. ACM 中忘记自己读二维数组

LeetCode:

复制代码
minPathSum(int[][] grid)

数组已经准备好了。

ACM:

复制代码
必须自己读 m、n
再 new int[m][n]
再双层 for 读取

二十、ACM 万能二维数组模板

建议背下来:

复制代码
import java.util.*;

public class Main {

    public static void main(String[] args) {

        Scanner in = new Scanner(System.in);

        int m = in.nextInt();
        int n = in.nextInt();

        int[][] grid = new int[m][n];

        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                grid[i][j] = in.nextInt();
            }
        }

        int ans = solve(grid);

        System.out.println(ans);
    }

    private static int solve(int[][] grid) {

        return 0;
    }
}

以后很多二维 ACM 题都可以直接套:

复制代码
岛屿
矩阵搜索
最短路径
网格 DP
二维遍历

二十一、面试怎么讲?

可以这样回答:

我定义 dp[i][j] 表示从左上角走到 (i,j) 的最小路径和。

因为题目规定只能向右或向下,所以到达 (i,j) 只可能从上方 (i-1,j) 或左方 (i,j-1) 过来。

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

第一行只能从左边过来,第一列只能从上面过来,所以单独初始化。

最终答案为 dp[m-1][n-1],时间复杂度 O(mn),空间复杂度 O(mn)

如果是 ACM 模式:

我会先通过 Scanner 读取 m、n,再通过双层 for 构造二维数组,调用算法后输出结果。


二十二、30 秒速记

看到:

复制代码
网格
只能向右 / 向下
求最小路径和

想到:

复制代码
二维 DP

定义:

复制代码
dp[i][j]
=
到当前格子的最小路径和

转移:

复制代码
dp[i][j] =
Math.min(dp[i - 1][j], dp[i][j - 1])
+ grid[i][j];

初始化:

复制代码
左上角:
grid[0][0]

第一行:
只能从左

第一列:
只能从上

答案:

复制代码
dp[m - 1][n - 1]

ACM 输入:

复制代码
int m = in.nextInt();
int n = in.nextInt();

int[][] grid = new int[m][n];

for (int i = 0; i < m; i++) {
    for (int j = 0; j < n; j++) {
        grid[i][j] = in.nextInt();
    }
}

一句话记忆:

最小路径和 = 当前格子只能从上或左过来,所以取 min(上, 左) + 当前值;ACM 模式再在外面套 Main + Scanner + 双层 for

相关推荐
彧azz2 小时前
算法设计与分析:贪心与动态规划
数据结构·学习·算法·贪心算法·动态规划
泡海椒2 小时前
PDF 表格样式优化:jquick-pdf 边框、圆角、背景色
java·开发语言·pdf
Beyond_System|系统之外3 小时前
【学编程】Python基础编程题100道(21-60)
开发语言·python·算法
景熙55233 小时前
15.Java 8 Stream 流入门到实战
java·开发语言·数据结构
wno7044 小时前
Spring Security权限控制
java·python·spring
杨运交4 小时前
[071][验证码模块]基于Spring拦截器的验证码认证设计思想
java·后端·spring
Geek-Chow4 小时前
CountDownLatch in Java
java
漂流瓶jz5 小时前
UVA-1442 洞穴 题解答案代码 算法竞赛入门经典第二版
c++·算法·题解·aoapc·算法竞赛入门经典·uva