解码方法 II
字数 3227 2025-12-24 13:27:51

好的,我随机挑选一个你列表中尚未出现过的线性动态规划题目。这次我们讲解:

解码方法 II


题目描述

给你一个只包含数字 '0' - '9' 和字符 '*' 的字符串 s。其中 '*' 可以表示 '1''9' 中的任意一个数字。

该字符串已经用一种特殊的编码规则进行了编码,规则如下:

  • 'A' -> 1
  • 'B' -> 2
  • ...
  • 'Z' -> 26

你需要计算出 总共有多少种方式 可以将这个已编码的字符串解码回一个只包含大写字母的字符串。

由于答案可能非常大,你需要将结果对 10^9 + 7 取模后返回。

示例 1:

输入:s = "*"
输出:9
解释:'*' 可以代表 '1' 到 '9' 中的任意一个,分别对应字母 A 到 I。所以有 9 种解码方式。

示例 2:

输入:s = "1*"
输出:18
解释:
第一种情况:把 '*' 当作单独的数字。
    '*' 作为 1-9:有 9 种(对应 "AA" 到 "AI")。
第二种情况:把 "1*" 当作一个两位数。
    "1*" 可以是 11-19(对应 "K" 到 "S"),共 9 种。
所以总共有 9 + 9 = 18 种。

示例 3:

输入:s = "2*"
输出:15
解释:
单独数字:'2' 固定, '*' 作为 1-9,共 9 种("BA" 到 "BI")。
两位数:"2*" 可以是 21-26(对应 "U" 到 "Z"),共 6 种。
总数:9 + 6 = 15。

解题思路分析

这是一个线性动态规划的经典变种,是 “解码方法” 问题的加强版。核心难点在于引入了通配符 '*',使得状态转移时需要根据当前字符和前一个字符的不同情况(数字、*)进行大量的条件分支处理。

1. 定义状态

dp[i] 表示字符串 s 的前 i 个字符(即 s[0..i-1])的解码方法总数。为了方便处理,我们让 dp[0] 表示空字符串的解码方法数,显然有 1 种(解码成一个空串)。dp[1] 表示第一个字符的解码方法数。

2. 寻找状态转移方程

我们需要考虑如何从 dp[i-1]dp[i-2] 推导出 dp[i]。解码到第 i 个字符时,有两种基本方式:

  • 方式一:将第 i 个字符单独解码为一个字母。这取决于 s[i-1](因为 dp[i] 对应 s[0..i-1])。
  • 方式二:将第 i-1 个字符和第 i 个字符组合解码为一个字母。这取决于 s[i-2]s[i-1] 组成的两位数是否在 1026 之间。

因此,dp[i] 应该等于这两种方式贡献的方法数之和。

3. 分类讨论(这是本题最核心、最繁琐的部分)

我们定义一个辅助函数 ways1(c) 表示单个字符 c 能解码成字母的方法数,以及 ways2(c1, c2) 表示两个字符 c1c2 组合解码成字母的方法数。

情况 A:单独解码(考虑 s[i-1]

  • 如果 s[i-1]'1''9'ways1 = 1
  • 如果 s[i-1]'*'ways1 = 9(因为可以代表 1-9)。
  • 如果 s[i-1]'0'ways1 = 0(因为 '0' 不能单独解码成任何字母,必须与前面的 '1''2' 组合)。
    这种方式的贡献是:dp[i] += ways1(s[i-1]) * dp[i-1]

情况 B:组合解码(考虑 s[i-2]s[i-1]
这需要判断两位数字符串 s[i-2]s[i-1] 是否在 "10""26" 之间。
我们需要根据 s[i-2]s[i-1] 的所有可能取值来计算 ways2

3.1 当 s[i-2] 是数字时:

  • s[i-2] == '1'
    • 如果 s[i-1]'0''9'ways2 = 1(因为 10-19 都有效)。
    • 如果 s[i-1]'*'ways2 = 9(因为 11-19 有效,共9种)。
  • s[i-2] == '2'
    • 如果 s[i-1]'0''6'ways2 = 1(因为 20-26 有效)。
    • 如果 s[i-1]'7''9'ways2 = 027-29 无效)。
    • 如果 s[i-1]'*'ways2 = 6(因为 21-26 有效,共6种)。
  • s[i-2]'0', '3''9'
    • 无论 s[i-1] 是什么,组合都是无效的(因为 00-09, 30-39, ... 90-99 都无效),ways2 = 0

3.2 当 s[i-2]'*' 时:
这时 s[i-2] 可以是 '1''2'(只有这两种可能让组合有效)。

  • 如果 s[i-1]'0''9'
    • 数字在 '0''6'ways2 = 2*'1''2' 都有效,例如 *0 可以是 1020)。
    • 数字是 '7''9'ways2 = 1* 只能为 '1' 才有效,例如 *7 只能是 17)。
  • 如果 s[i-1]'*'
    • ** 的组合:11-19(9种)和 21-26(6种)都有效,共 15 种。所以 ways2 = 15

这种方式的贡献是:dp[i] += ways2(s[i-2], s[i-1]) * dp[i-2]

4. 初始化和边界条件

  • dp[0] = 1(空串有一种解码方式)。
  • 对于 dp[1],我们只需要应用 情况 A 的规则(单独解码第一个字符),因为不存在 s[-1] 来组合。
  • i = 2 开始,我们同时考虑情况 A 和情况 B。

5. 最终答案与取模
最终答案为 dp[n],其中 n 是字符串 s 的长度。每一步计算都需要对 10^9 + 7 取模,防止溢出。


详细解题步骤与代码逻辑

我们将上述思路转化为伪代码,并最终写成具体实现。

  1. 定义常量 MOD = 10**9 + 7
  2. 初始化 dp 数组,长度为 n+1dp[0] = 1
  3. 计算 dp[1]
    • 如果 s[0] == '*'dp[1] = 9
    • 如果 s[0] == '0'dp[1] = 0
    • 否则('1''9'):dp[1] = 1
  4. 循环 i2n
    • 初始化 count = 0
    • 情况 A:单独解码 s[i-1]
      • 调用 ways1(s[i-1]),乘以 dp[i-1],加到 count
    • 情况 B:组合解码 s[i-2]s[i-1]
      • 调用 ways2(s[i-2], s[i-1]),乘以 dp[i-2],加到 count
    • dp[i] = count % MOD
  5. 返回 dp[n]

其中 ways1ways2 函数根据上面的分类讨论实现。


代码实现示例 (Python)

class Solution:
    def numDecodings(self, s: str) -> int:
        MOD = 10**9 + 7
        n = len(s)
        dp = [0] * (n + 1)
        dp[0] = 1  # 空字符串

        # 初始化第一个字符
        if s[0] == '*':
            dp[1] = 9
        elif s[0] == '0':
            dp[1] = 0
        else:
            dp[1] = 1

        for i in range(2, n + 1):
            # 情况A: 单独解码最后一个字符 s[i-1]
            if s[i-1] == '*':
                count = 9 * dp[i-1]
            elif s[i-1] != '0':
                count = dp[i-1]
            else:
                count = 0  # '0' 不能单独解码

            # 情况B: 组合解码最后两个字符 s[i-2], s[i-1]
            if s[i-2] == '1':
                if s[i-1] == '*':
                    count += 9 * dp[i-2]
                else:
                    count += dp[i-2]  # 10-19 都有效
            elif s[i-2] == '2':
                if s[i-1] == '*':
                    count += 6 * dp[i-2]  # 21-26 共6种
                elif '0' <= s[i-1] <= '6':
                    count += dp[i-2]  # 20-26 有效
            elif s[i-2] == '*':
                if s[i-1] == '*':
                    count += 15 * dp[i-2]  # 11-19 和 21-26, 共15种
                elif '0' <= s[i-1] <= '6':
                    count += 2 * dp[i-2]  # *0-*6: *为1或2都有效
                elif '7' <= s[i-1] <= '9':
                    count += dp[i-2]  # *7-*9: *只能为1才有效

            dp[i] = count % MOD

        return dp[n]

关键点总结与复杂度分析

  1. 核心思想:仍然是经典的“爬楼梯”类动态规划,但状态转移时的“步长价值”(ways1ways2)变得异常复杂,需要对 '*' 进行枚举分析。
  2. 状态定义dp[i] 表示前 i 个字符的解码方法数,这是处理此类“分段”问题的标准定义。
  3. 分类讨论:这是本题最考验细心和逻辑严密性的地方。必须清晰地列出 s[i-1]s[i-2] 所有可能的字符组合(数字、*),并计算出每种组合对应的解码方法数。
  4. 初始化:特别注意 dp[0] = 1 和第一个字符 '0' 的特殊情况。
  5. 复杂度
    • 时间复杂度O(n),我们只遍历了一次字符串。
    • 空间复杂度O(n),用于存储 dp 数组。可以优化为 O(1),因为每个状态只依赖于前两个状态(滚动数组)。

通过这样逐步拆解,我们就将一道看似复杂的题目,转化为了一个思路清晰、逻辑严密的动态规划问题。掌握这种分类讨论分情况计算贡献的能力,是解决此类带通配符或复杂约束的动态规划问题的关键。

好的,我随机挑选一个你列表中尚未出现过的线性动态规划题目。这次我们讲解: 解码方法 II 题目描述 给你一个只包含数字 '0' - '9' 和字符 '*' 的字符串 s 。其中 '*' 可以表示 '1' 到 '9' 中的任意一个数字。 该字符串已经用一种特殊的编码规则进行了编码,规则如下: 'A' -> 1 'B' -> 2 ... 'Z' -> 26 你需要计算出 总共有多少种方式 可以将这个已编码的字符串解码回一个只包含大写字母的字符串。 由于答案可能非常大,你需要将结果对 10^9 + 7 取模后返回。 示例 1: 示例 2: 示例 3: 解题思路分析 这是一个线性动态规划的经典变种,是 “解码方法” 问题的加强版。核心难点在于引入了通配符 '*' ,使得状态转移时需要根据当前字符和前一个字符的不同情况(数字、 * )进行大量的条件分支处理。 1. 定义状态 设 dp[i] 表示字符串 s 的前 i 个字符(即 s[0..i-1] )的解码方法总数。为了方便处理,我们让 dp[0] 表示空字符串的解码方法数,显然有 1 种 (解码成一个空串)。 dp[1] 表示第一个字符的解码方法数。 2. 寻找状态转移方程 我们需要考虑如何从 dp[i-1] 和 dp[i-2] 推导出 dp[i] 。解码到第 i 个字符时,有两种基本方式: 方式一:将第 i 个字符单独解码 为一个字母。这取决于 s[i-1] (因为 dp[i] 对应 s[0..i-1] )。 方式二:将第 i-1 个字符和第 i 个字符组合解码 为一个字母。这取决于 s[i-2] 和 s[i-1] 组成的两位数是否在 10 到 26 之间。 因此, dp[i] 应该等于这两种方式贡献的方法数之和。 3. 分类讨论(这是本题最核心、最繁琐的部分) 我们定义一个辅助函数 ways1(c) 表示单个字符 c 能解码成字母的方法数,以及 ways2(c1, c2) 表示两个字符 c1 和 c2 组合解码成字母的方法数。 情况 A:单独解码(考虑 s[i-1] ) 如果 s[i-1] 是 '1' 到 '9' : ways1 = 1 。 如果 s[i-1] 是 '*' : ways1 = 9 (因为可以代表 1-9)。 如果 s[i-1] 是 '0' : ways1 = 0 (因为 '0' 不能单独解码成任何字母,必须与前面的 '1' 或 '2' 组合)。 这种方式的贡献是: dp[i] += ways1(s[i-1]) * dp[i-1] 。 情况 B:组合解码(考虑 s[i-2] 和 s[i-1] ) 这需要判断两位数字符串 s[i-2]s[i-1] 是否在 "10" 到 "26" 之间。 我们需要根据 s[i-2] 和 s[i-1] 的所有可能取值来计算 ways2 。 3.1 当 s[i-2] 是数字时: s[i-2] == '1' : 如果 s[i-1] 是 '0' 到 '9' : ways2 = 1 (因为 10-19 都有效)。 如果 s[i-1] 是 '*' : ways2 = 9 (因为 11-19 有效,共9种)。 s[i-2] == '2' : 如果 s[i-1] 是 '0' 到 '6' : ways2 = 1 (因为 20-26 有效)。 如果 s[i-1] 是 '7' 到 '9' : ways2 = 0 ( 27-29 无效)。 如果 s[i-1] 是 '*' : ways2 = 6 (因为 21-26 有效,共6种)。 s[i-2] 是 '0' , '3' 到 '9' : 无论 s[i-1] 是什么,组合都是无效的(因为 00-09 , 30-39 , ... 90-99 都无效), ways2 = 0 。 3.2 当 s[i-2] 是 '*' 时: 这时 s[i-2] 可以是 '1' 或 '2' (只有这两种可能让组合有效)。 如果 s[i-1] 是 '0' 到 '9' : 数字在 '0' 到 '6' : ways2 = 2 ( * 为 '1' 或 '2' 都有效,例如 *0 可以是 10 或 20 )。 数字是 '7' 到 '9' : ways2 = 1 ( * 只能为 '1' 才有效,例如 *7 只能是 17 )。 如果 s[i-1] 是 '*' : ** 的组合: 11-19 (9种)和 21-26 (6种)都有效,共 15 种。所以 ways2 = 15 。 这种方式的贡献是: dp[i] += ways2(s[i-2], s[i-1]) * dp[i-2] 。 4. 初始化和边界条件 dp[0] = 1 (空串有一种解码方式)。 对于 dp[1] ,我们只需要应用 情况 A 的规则(单独解码第一个字符),因为不存在 s[-1] 来组合。 从 i = 2 开始,我们同时考虑情况 A 和情况 B。 5. 最终答案与取模 最终答案为 dp[n] ,其中 n 是字符串 s 的长度。每一步计算都需要对 10^9 + 7 取模,防止溢出。 详细解题步骤与代码逻辑 我们将上述思路转化为伪代码,并最终写成具体实现。 定义常量 MOD = 10**9 + 7 。 初始化 dp 数组,长度为 n+1 , dp[0] = 1 。 计算 dp[1] : 如果 s[0] == '*' : dp[1] = 9 如果 s[0] == '0' : dp[1] = 0 否则( '1' 到 '9' ): dp[1] = 1 循环 i 从 2 到 n : 初始化 count = 0 。 情况 A :单独解码 s[i-1] 。 调用 ways1(s[i-1]) ,乘以 dp[i-1] ,加到 count 。 情况 B :组合解码 s[i-2] 和 s[i-1] 。 调用 ways2(s[i-2], s[i-1]) ,乘以 dp[i-2] ,加到 count 。 dp[i] = count % MOD 。 返回 dp[n] 。 其中 ways1 和 ways2 函数根据上面的分类讨论实现。 代码实现示例 (Python) 关键点总结与复杂度分析 核心思想 :仍然是经典的“爬楼梯”类动态规划,但状态转移时的“步长价值”( ways1 和 ways2 )变得异常复杂,需要对 '*' 进行枚举分析。 状态定义 : dp[i] 表示前 i 个字符的解码方法数,这是处理此类“分段”问题的标准定义。 分类讨论 :这是本题最考验细心和逻辑严密性的地方。必须清晰地列出 s[i-1] 和 s[i-2] 所有可能的字符组合(数字、 * ),并计算出每种组合对应的解码方法数。 初始化 :特别注意 dp[0] = 1 和第一个字符 '0' 的特殊情况。 复杂度 : 时间复杂度 : O(n) ,我们只遍历了一次字符串。 空间复杂度 : O(n) ,用于存储 dp 数组。可以优化为 O(1) ,因为每个状态只依赖于前两个状态(滚动数组)。 通过这样逐步拆解,我们就将一道看似复杂的题目,转化为了一个思路清晰、逻辑严密的动态规划问题。掌握这种 分类讨论 和 分情况计算贡献 的能力,是解决此类带通配符或复杂约束的动态规划问题的关键。