目录
[1. 最小路径和](#1. 最小路径和)
[1.1 题目解析](#1.1 题目解析)
[1.2 解法](#1.2 解法)
[1.3 代码实现](#1.3 代码实现)
[2. 地下城游戏](#2. 地下城游戏)
[2.1 题目解析](#2.1 题目解析)
[2.2 解法](#2.2 解法)
[2.3 代码实现](#2.3 代码实现)
1. 最小路径和
https://leetcode.cn/problems/minimum-path-sum/description/
给定一个包含非负整数的 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
1.1 题目解析
题目本质
这是"选择最小值"问题,不是"加最小值"问题。 对比下降路径最小和,那道题第一行的三个方向都来自 dp[0][],全是 0,加起来还是当前格子值,不影响结果 。但最小路径和的第一行会从左边累加(dp[1][1] → dp[1][2] → dp[1][3]),如果 dp[0][j] = 0,会被 Math.min 错误选中
常规解法
最直观的想法是用递归/DFS枚举所有路径:从起点开始,每个位置尝试向右和向下两个方向,递归到终点时记录路径和,最后取最小值。
问题分析
纯递归会超时。对于 200×200 的网格,最坏情况时间复杂度接近 O(2^400),且存在大量重复计算。比如计算到达 (10,10) 的最小路径和会被多次重复计算。
思路转折
要想高效 → 必须消除重复计算 → 使用动态规划。
关键观察:到达某个格子 (i,j) 的最小路径和 = 当前格子的值 + min(从左边来的最小路径和, 从上边来的最小路径和)。注意这里是"选择"左边或上边中更小的那个,而不是"累加"。通过自底向上填表,每个状态只计算一次。
1.2 解法
算法思想
使用二维 DP,定义 dp[i][j] 表示从起点到达位置 (i,j) 的最小路径和。
递推公式:
dp[i][j] = grid[i-1][j-1] + min(dp[i-1][j], dp[i][j-1])
边界条件:
- dp[1][1] = grid[0][0](起点)
- 第 0 行和第 0 列初始化为 MAX_VALUE(不可达)
**i)**创建 (m+1) × (n+1) 的 dp 数组,使用 1-索引
**ii)**关键:将整个 dp 数组初始化为 Integer.MAX_VALUE
**iii)**单独初始化起点 dp[1][1] = grid[0][0]
**iv)**双层循环遍历所有格子,按递推公式取最小值
**v)**返回 dp[m][n] 作为答案
易错点
- **索引映射混淆:**dp 数组用 1-索引,原数组用 0-索引。访问原数组时要用 grid[i-1][j-1]
- **起点处理:**必须跳过起点单独赋值,否则会尝试从"上边"或"左边"取值,导致错误
1.3 代码实现
java
static class Solution {
int[][] dp;
public int minPathSum(int[][] grid) {
int m = grid.length, n = grid[0].length;
dp = new int[m + 1][n + 1];
// 核心:全部初始化为无穷大
// 原因:这是"选择最小值"问题,0 会被 Math.min 错误选中
for (int i = 0; i <= m; i++) {
for (int j = 0; j <= n; j++) {
dp[i][j] = Integer.MAX_VALUE;
}
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (i == 1 && j == 1) {
dp[i][j] = grid[0][0]; // 起点直接赋值
continue;
}
// 当前最小路径和 = 当前格子 + min(上边来, 左边来)
dp[i][j] = grid[i - 1][j - 1] + Math.min(dp[i][j - 1], dp[i - 1][j]);
}
}
return dp[m][n];
}
}
复杂度分析
- **时间复杂度:O(m × n),**需要遍历整个网格一次
- **空间复杂度:O(m × n),**使用了二维 dp 数组
2. 地下城游戏
https://leetcode.cn/problems/dungeon-game/description/
恶魔们抓住了公主并将她关在了地下城 dungeon
的 右下角 。地下城是由 m x n
个房间组成的二维网格。我们英勇的骑士最初被安置在 左上角 的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主。
骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡。
有些房间由恶魔守卫,因此骑士在进入这些房间时会失去健康点数(若房间里的值为负整数 ,则表示骑士将损失健康点数);其他房间要么是空的(房间里的值为 0 ),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)。
为了尽快解救公主,骑士决定每次只 向右 或 向下 移动一步。
返回确保骑士能够拯救到公主所需的最低初始健康点数。
**注意:**任何房间都可能对骑士的健康点数造成威胁,也可能增加骑士的健康点数,包括骑士进入的左上角房间以及公主被监禁的右下角房间。
示例 1:

输入:dungeon = [[-2,-3,3],[-5,-10,1],[10,30,-5]]
输出:7
解释:如果骑士遵循最佳路径:右 -> 右 -> 下 -> 下 ,则骑士的初始健康点数至少为 7 。
示例 2:
输入:dungeon = [[0]]
输出:1
提示:
m == dungeon.length
n == dungeon[i].length
1 <= m, n <= 200
-1000 <= dungeon[i][j] <= 1000
2.1 题目解析
题目本质
骑士从左上角到右下角救公主,每个房间会增减血量,关键是任何时刻血量都不能 ≤ 0。目标是求初始最少需要多少血才能保证全程存活,不同于普通的路径和问题。
常规解法
最直观的想法是正向 DP:定义 dp[i][j] 表示到达 (i,j) 时的最大剩余血量,然后选择剩余血量最大的路径,最后倒推初始需要多少血。
问题分析
正向 DP 无法解决这道题。
你在中间没法判断:
-
不知道"现在血多"是因为"起点带得多"还是"一路加得多"
-
不知道"后面需要多少血"才能不死
思路转折
从终点倒推:下一步要X血,这里加/扣Y血,那我现在要max(1, X-Y)血,推到起点就是答案。
2.2 解法
用二维 DP 从右下往左上逆向推导,定义 dp[i][j] 表示从位置 (i,j) 出发到达终点所需的最少血量。
递推公式:
java
next = min(dp[i+1][j], dp[i][j+1]) // 选择右边或下边需求更少的路径
dp[i][j] = max(1, next - dungeon[i][j])
边界条件:
- 数组大小:(m+2) × (n+2),为右边和下边留出边界
- 边界初始化为 MAX_VALUE(不可达)
- 终点的右边和下边设为 1:dp[m][n+1] = dp[m+1][n] = 1
**i)**创建 (m+2) × (n+2) 的 dp 数组,使用 1-索引,多留一行一列给右边和下边
**ii)**将整个 dp 数组初始化为 Integer.MAX_VALUE(表示不可达)
**iii)**设置虚拟边界:dp[m][n+1] = 1 和 dp[m+1][n] = 1,表示"通关后"至少要 1 点血
**iv)**从右下往左上遍历:外层循环 i 从 m 到 1,内层循环 j 从 n 到 1
**v)**每个位置取右边和下边的最小需求,倒推当前位置的需求
**vi)**返回 dp[1][1](起点需要的血量)
易错点
- **DP 方向错误:**这道题必须从右下往左上推,正向 DP 无法得到正确答案。循环必须是 i = m → 1 和 j = n → 1
- **边界设置方向错误:要设置终点的右边 dp[m][n+1] 和下边 dp[m+1][n],**不是左边或上边。因为逆向推导时,终点会访问 dp[i][j+1] 和 dp[i+1][j]
- **数组大小不够:**必须开 [m+2][n+2],不能是 [m+1][n+1],否则访问 dp[m][n+1] 和 dp[m+1][n] 会越界
- **忘记 max(1, ...):**即使当前房间加血很多(如 +100),理论计算可能得到 ≤0 的值,但必须保证进入任何房间前至少 1 点血。公式中的 max(1, ...) 不能省略
- 减法的理解: next - dungeon[i][j] 中,如果当前房间是正数(加血),需求会减少 ;如果是负数(扣血),需求会增加。当 next - dungeon[i][j] ≤ 0 时,说明当前房间加的血 ≥ 后面需要的血,带 1 点血进入就够了
2.3 代码实现
java
static class Solution {
// 核心逻辑:你当前格子是 Y,下一个格子需要 X 点血
// 那你进入当前格子前需要:max(1, X - Y)
int[][] dp;
public int calculateMinimumHP(int[][] num) {
int m = num.length, n = num[0].length;
dp = new int[m + 2][n + 2]; // 多开一行一列给右边和下边
// 边界初始化为无穷大(不可达区域)
for (int i = 0; i <= m + 1; i++) {
for (int j = 0; j <= n + 1; j++) {
dp[i][j] = Integer.MAX_VALUE;
}
}
// 终点的右边和下边设为 1
// 意思:通关后的"虚拟下一步"需要 1 点血(保证终点计算正确)
dp[m][n + 1] = 1;
dp[m + 1][n] = 1;
// 从右下往左上推导
for (int i = m; i >= 1; i--) {
for (int j = n; j >= 1; j--) {
// 下一步需求:选择右边或下边需求更少的
int next = Math.min(dp[i][j + 1], dp[i + 1][j]);
// 当前需求 = max(1, 下一步需求 - 当前房间值)
dp[i][j] = Math.max(1, next - num[i - 1][j - 1]);
}
}
return dp[1][1]; // 起点需要的血量
}
}
复杂度分析
- **时间复杂度:O(m × n),**需要遍历整个网格一次
- **空间复杂度:O(m × n),**使用了二维 dp 数组