好的,我随机挑选一个你列表中尚未出现过的线性动态规划题目。这次我们讲解:
解码方法 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]组成的两位数是否在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)
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]
关键点总结与复杂度分析
- 核心思想:仍然是经典的“爬楼梯”类动态规划,但状态转移时的“步长价值”(
ways1和ways2)变得异常复杂,需要对'*'进行枚举分析。 - 状态定义:
dp[i]表示前i个字符的解码方法数,这是处理此类“分段”问题的标准定义。 - 分类讨论:这是本题最考验细心和逻辑严密性的地方。必须清晰地列出
s[i-1]和s[i-2]所有可能的字符组合(数字、*),并计算出每种组合对应的解码方法数。 - 初始化:特别注意
dp[0] = 1和第一个字符'0'的特殊情况。 - 复杂度:
- 时间复杂度:
O(n),我们只遍历了一次字符串。 - 空间复杂度:
O(n),用于存储dp数组。可以优化为O(1),因为每个状态只依赖于前两个状态(滚动数组)。
- 时间复杂度:
通过这样逐步拆解,我们就将一道看似复杂的题目,转化为了一个思路清晰、逻辑严密的动态规划问题。掌握这种分类讨论和分情况计算贡献的能力,是解决此类带通配符或复杂约束的动态规划问题的关键。