详细介绍:LeetCode算法日记 - Day 70: 使用最小花费爬楼梯、解码方法
目录
1. 使用最小花费爬楼梯
https://leetcode.cn/problems/min-cost-climbing-stairs/description/
给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。
请你计算并返回达到楼梯顶部的最低花费。
示例 1:
输入:cost = [10,15,20]
输出:15
解释:你将从下标为 1 的台阶开始。
- 支付 15 ,向上爬两个台阶,到达楼梯顶部。
总花费为 15 。
示例 2:
输入:cost = [1,100,1,1,1,100,1,1,100,1]
输出:6
解释:你将从下标为 0 的台阶开始。
- 支付 1 ,向上爬两个台阶,到达下标为 2 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 4 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 6 的台阶。
- 支付 1 ,向上爬一个台阶,到达下标为 7 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 9 的台阶。
- 支付 1 ,向上爬一个台阶,到达楼梯顶部。
总花费为 6 。
提示:
2 <= cost.length <= 10000 <= cost[i] <= 999
1.1 题目解析
题目本质
路径选择中的最小代价问题。在爬楼梯的过程中,每次可以选择走1步或2步,求到达终点的最小总花费。本质是"多阶段决策优化"。
常规解法
递归枚举所有可能的爬法。从起点开始,每次决定爬1步还是2步,递归计算所有路径的花费,取最小值。
问题分析
递归会产生大量重复计算。例如到达第5个台阶,可能从第3或第4个台阶跳过来,这两条路径都需要重复计算"到达第5个台阶后的最优解"。时间复杂度为 O(2^n),对于1000个台阶会超时。
思路转折
既然子问题重复 → 必须记录中间结果 → 动态规划。从前往后推导,用数组保存"到达每个位置的最小花费"。
关键理解:题目要求的是离开台阶的花费,不是到达台阶的花费。站在某个台阶上不花钱,离开时才支付。最终目标是到达"楼梯顶部"(超过最后一个台阶的位置),可以从倒数第一或倒数第二个台阶跳上去。
1.2 解法
算法思想
动态规划。定义 dp[i] 为到达第 i 个位置的最小花费。
状态转移方程:
dp[i] = min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2])
到达位置 i 有两种方式:
从位置 i-1 跳1步:需要先到达 i-1(花费 dp[i-1]),然后支付离开 i-1 的费用(cost[i-1])
从位置 i-2 跳2步:需要先到达 i-2(花费 dp[i-2]),然后支付离开 i-2 的费用(cost[i-2])
初始状态:dp[0] = 0, dp[1] = 0(可以免费站在前两个台阶)
i)创建 dp 数组,长度为 n+1(n 是台阶数,n+1 表示楼梯顶部)
ii)初始化 dp[0] = 0, dp[1] = 0(题目允许从索引0或1免费开始)
iii)从位置2开始遍历到位置n:
计算从位置 i-1 跳过来的花费:dp[i-1] + cost[i-1]
计算从位置 i-2 跳过来的花费:dp[i-2] + cost[i-2]
取两者最小值赋给 dp[i]
iv)返回 dp[n](楼梯顶部的最小花费)
易错点
费用的支付时机:cost[i] 是离开第 i 个台阶的费用,不是到达的费用。从台阶 i 跳走时才支付 cost[i]
数组索引混淆:dp 数组大小是 n+1,因为要表示"楼梯顶部"这个额外位置。dp[i] 对应的台阶费用是 cost[i-1] 和 cost[i-2]
初始状态理解:dp[0] 和 dp[1] 都是0,表示可以免费站在前两个台阶上(还没离开,所以不用付费)
目标位置:目标不是到达最后一个台阶(索引 n-1),而是到达楼梯顶部(索引 n,超出数组范围的虚拟位置)
1.3 代码实现
class Solution {
int[] dp;
public int minCostClimbingStairs(int[] cost) {
int n = cost.length;
dp = new int[n + 1];
// 可以免费站在前两个台阶
dp[0] = 0;
dp[1] = 0;
// 从位置2开始计算到楼梯顶部
for(int i = 2; i <= n; i++){
// 从i-1跳1步 或 从i-2跳2步,取最小值
dp[i] = Math.min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2]);
}
return dp[n];
}
}
复杂度分析
时间复杂度:O(n),遍历一次所有位置,每个位置做常数次比较。
空间复杂度:O(n),使用 dp 数组存储每个位置的最小花费。可优化到 O(1)(只保留前两个状态值)。
2. 解码方法
https://leetcode.cn/problems/decode-ways/description/
一条包含字母 A-Z 的消息通过以下映射进行了 编码 :
"1" -> 'A'
"2" -> 'B'
...
"25" -> 'Y'
"26" -> 'Z'
然而,在 解码 已编码的消息时,你意识到有许多不同的方式来解码,因为有些编码被包含在其它编码当中("2" 和 "5" 与 "25")。
例如,"11106" 可以映射为:
"AAJF",将消息分组为(1, 1, 10, 6)"KJF",将消息分组为(11, 10, 6)- 消息不能分组为
(1, 11, 06),因为"06"不是一个合法编码(只有 "6" 是合法的)。
注意,可能存在无法解码的字符串。
给你一个只含数字的 非空 字符串 s ,请计算并返回 解码 方法的 总数 。如果没有合法的方式解码整个字符串,返回 0。
题目数据保证答案肯定是一个 32 位 的整数。
示例 1:
输入:s = "12"
输出:2
解释:它可以解码为 "AB"(1 2)或者 "L"(12)。
示例 2:
输入:s = "226"
输出:3
解释:它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。
示例 3:
输入:s = "06"
输出:0
解释:"06" 无法映射到 "F" ,因为存在前导零("6" 和 "06" 并不等价)。
2.1 题目解析
题目本质
计数问题。给定数字字符串,按照1-26的编码规则,统计有多少种不同的分组解码方式。核心在于"分割字符串的方案数统计"。
常规解法
递归枚举所有可能的分割方式。对于每个位置,尝试单独解码(1位)或组合解码(2位),递归计算后续部分的方案数。
问题分析
递归会产生大量重复子问题。例如"226"中,无论前面选"2"还是"22",都要重复计算"6"开始的方案数。时间复杂度达到 O(2^n),对于长字符串会超时。
思路转折
既然子问题重复出现 → 必须避免重复计算 → 动态规划。用数组保存每个位置的解码方案数,从前往后推导,每个位置只计算一次。
关键是找到状态转移:当前位置的方案数 = 当前位置单独解码的方案数 + 与前一个位置组合解码的方案数。
2.2 解法
算法思想
动态规划。定义 dp[i] 为前 i+1 个字符的解码方案数。
状态转移方程:
如果第 i 个字符可以单独解码(1-9):dp[i] += dp[i-1]
如果第 i-1 和第 i 个字符可以组合解码(10-26):dp[i] += dp[i-2]
边界条件:
dp[0] = 1(第一个字符非0时)
遇到'0'必须和前一个字符组成10或20,否则无法解码
i)检查首字符是否为'0',如果是直接返回0(无法解码)
ii)初始化 dp[0] = 1(第一个字符的方案数)
iii)处理第二个字符(如果存在):
判断能否单独解码(不是'0')
判断能否和第一个字符组合(10-26)
累加方案数到 dp[1]
iv)从第三个字符开始遍历:
判断当前字符能否单独解码(1-9),若能则 dp[i] += dp[i-1]
判断当前字符能否和前一个组合(10-26),若能则 dp[i] += dp[i-2]
如果两种方式都不行(dp[i] == 0),返回0
v)返回 dp[n-1]
易错点
'0' 的处理:'0' 不能单独解码,只能和前面的'1'或'2'组成10或20。例如"06"、"30"都无法解码
组合解码的范围:必须是10-26,不能只判断 ≤26。例如"01"、"03"虽然 ≤26 但不是有效编码
边界检查:处理第二个字符前要先判断字符串长度是否 >1,避免数组越界
2.3 代码实现
class Solution {
int[] dp;
public int numDecodings(String s) {
dp = new int[s.length()];
char[] ch = s.toCharArray();
// 首字符为0无法解码
if(ch[0] == '0') return 0;
dp[0] = 1;
// 处理第二个字符
if(ch.length > 1){
int twoDigits = (ch[0] - '0') * 10 + (ch[1] - '0');
// 组合解码(10-26)
if(twoDigits >= 10 && twoDigits <= 26) dp[1]++;
// 单独解码(1-9)
if(ch[1] != '0') dp[1]++;
if(dp[1] == 0) return 0;
}
// 处理后续字符
for(int i = 2; i < ch.length; i++){
int twoDigits = (ch[i-1] - '0') * 10 + (ch[i] - '0');
// 单独解码
if(ch[i] - '0' >= 1 && ch[i] - '0' <= 9) {
dp[i] += dp[i-1];
}
// 组合解码
if(twoDigits >= 10 && twoDigits <= 26) {
dp[i] += dp[i-2];
}
// 两种方式都不行
if(dp[i] == 0) return 0;
}
return dp[ch.length - 1];
}
}
复杂度分析
时间复杂度:O(n),只需遍历字符串一次,每个位置做常数次判断。
空间复杂度:O(n),使用 dp 数组存储每个位置的方案数。可优化到 O(1)(只保留前两个状态)。
浙公网安备 33010602011771号