详细介绍:LeetCode算法日记 - Day 70: 使用最小花费爬楼梯、解码方法

目录

1. 使用最小花费爬楼梯

1.1 题目解析

1.2 解法

1.3 代码实现

2. 解码方法 

2.1 题目解析

2.2 解法

2.3 代码实现


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 <= 1000
  • 0 <= 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)(只保留前两个状态)。

posted on 2025-11-09 22:15  blfbuaa  阅读(11)  评论(0)    收藏  举报