多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

AlgoNote 题解:0639. 解码方法 II —— 带通配符 `*` 的线性 DP 状态转移全解析

AlgoNote 题解:0639. 解码方法 II —— 带通配符 `*` 的线性 DP 状态转移全解析 教程文档知识库【免费下载链接】AlgoNote⛽️「算法通关手册」从零开始的「算法与数据结构」学习教程200 道「算法面试热门题目」1000 道「LeetCode 题目解析」持续更新中项目地址https://gitcode.com/gh_mirrors/le/AlgoNote点击查看免费下载本文是「算法通关手册」AlgoNote题解体系的组成部分围绕 0639. 解码方法 II 题解 展开。这道题在 LeetCode 中属于「字符串 动态规划」分类、难度为困难是 0091. 解码方法中等的升级版本原题只处理纯数字串本题额外引入通配符*使状态转移的条件与系数成倍增加。读完本文你将掌握一类「带通配符的计数型线性 DP」的完整建模方法如何用parse1 / parse2两个辅助函数把七种字符组合情况拆解得清晰可控如何基于「前 i 个字符」状态做递推以及为什么要对10^9 7取模。一、问题定义从数字到字母的反向映射题目给定一个仅由数字0 ~ 9与通配符*组成的字符串s它已经按照如下映射关系编码A映射为1B映射为2……Z映射为26其中*是一个特殊通配符可以表示1 ~ 9中的任意一个数字注意不包含0。例如字符串1*可以等价于11、12、…、18、19中的任意一个编码。解码过程就是把编码串反向翻译回字母串。例如11106有两种分组方式AAJF分组为(1 1 10 6)KJF分组为(11 10 6)题目要求计算出字符串s总共有多少种可能的解码方案。示例摘自原题解文档输入s * 输出9 解释这一条编码消息可以表示 1、2、3、4、5、6、7、8 或 9 中的任意一条。 可以分别解码成字符串 A、B、C、D、E、F、G、H 和 I 。 因此* 总共有 9 种解码方法。数据范围与约束原文档说明1 s.length 100s只包含数字并且可能包含前导零例如01这类情况需要靠状态转移自然淘汰题目数据保证答案一定是一个 32 位整数由于答案可能很大代码中需要对10^9 7取模这也对应了原题解代码中的mod 10 ** 9 7。二、从 0091 到 0639问题的升级脉络理解本题最好的起点是它的「弟弟题」0091. 解码方法。在 0091 中输入只有数字字符状态转移只有两条来源用一个字符s[i]翻译只要s[i] ! 0即可翻译为A ~ I中某个字母方案数为dp[i - 1]用两个字符s[i - 1]、s[i]翻译要求s[i - 1] ! 0且两字符组成的整数 26可翻译为J ~ Z中某个字母方案数为dp[i - 2]。两条来源可能同时成立、也可能都不成立转移时把成立项的方案数累加即可。而在 0639 中字符集合里混入了*。*的出现让「一个字符能译出几种字母」「两个字符能译出几种字母」都变成了一个区间内可枚举的计数问题原本简单的1变成了×9、×15、×6等乘法系数。正如原题解文档所强调的本题的状态转移方程的条件和公式不太容易想全——这正是本篇文章要重点拆解的部分。从仓库的分类体系看两道题都被收录在「字符串、动态规划」分类下其中 0091 为中等、0639 为困难参见 分类题目列表 与 全部题目列表。三、动态规划五步建模本题属于单串线性 DP输入只有一个字符串经典的状态定义就是「前i个字符构成的子问题解」对应线性 DP 中「由前i个元素组成的子数组」这一状态语义参见 单串线性 DP 讲解。下面严格按「阶段划分 → 定义状态 → 状态转移 → 初始条件 → 最终结果」五步展开这套方法论与 动态规划基础 中给出的通用流程一致。3.1 阶段划分按照字符串的结尾位置进行阶段划分从左到右逐个字符扩展前缀每处理完一个字符就是一个阶段。3.2 定义状态定义状态dp[i]表示字符串s前i个字符构成的字符串可能构成的翻译方案数。这里采用「长度为i的前缀」而非「以第i个字符结尾」的语义好处是dp[0]可以自然表示空串便于初始化与边界处理——与仓库中单串线性 DP 第 3 种状态定义的惯例一致。3.3 状态转移方程两类来源、七种情况dp[i]的方案数来自两条路径的累加路径 A使用一个字符翻译s[i]条件含义方案系数s[i] *可视为[1, 9]任意数字翻译为A ~ I9即dp[i] dp[i-1] * 9s[i] 0单字符0无法对应任何字母0即dp[i] dp[i-1] * 0s[i]为[1, 9]中某个数字可翻译为对应位置的某个字母1即dp[i] dp[i-1] * 1路径 B使用两个字符翻译s[i-1]与s[i]这里需要同时考虑前一个字符与当前字符是否是*共细分为 7 种情况条件分析方案系数s[i-1] *且s[i] *两位数可落在[11, 19]或[21, 26]共9 6 15种15s[i-1] *且s[i]在[1, 6]内十位可选1或22s[i-1] *且s[i]不在[1, 6]内即7/8/9十位只能选127/28/29均超范围1s[i-1] 1且s[i] *个位可取[1, 9]任意数字9s[i-1] 1且s[i] ! *个位为具体数字一定落在合法区间1s[i-1] 2且s[i] *个位只能取[1, 6]6s[i-1] 2且s[i]在[1, 6]内落在[21, 26]合法1s[i-1] 2且s[i]不在[1, 6]内20 ~ 29中超出 26 的部分非法含0、7/8/90s[i-1]为[3, 9]中某个数字非*与任何个位组合都超过 26无法双字符翻译0合并之后核心系数表就是** → 15* 小数字 → 2* 大数字 → 11* → 91 数字 → 12* → 62 [1,6] → 12 其他 → 0其余 → 0。记忆口诀可以概括为「星号乘区间、一配全、二配半」。转移时将路径 A 与路径 B 的贡献相加并在每一步对10^9 7取模防止中间结果溢出与答案失真。为了让这 9 组判断逻辑在代码中清晰可维护原题解建议把「单个字符s[i]的翻译方案数」与「两个字符s[i-1]、s[i]的翻译方案数」分别封装成两个方法即下文源码中的parse1与parse2。3.4 初始条件dp[0] 1空字符串只有一种翻译方案翻译为空串作为递推的种子。dp[1] parse1(s[0])只有一个字符时直接套用「单字符翻译方案数」的求法*为 90为 0其余为 1。注意dp[1]不能用循环内统一公式硬套因为当i 1时不存在dp[-1]所以单独初始化是必要的边界处理。3.5 最终结果根据状态定义dp[size]size len(s)即为整个字符串的解码方案总数直接返回即可。四、源码级实现解析原题解给出的完整 Python 实现如下路径decode-ways-ii.mdclass Solution: def parse1(self, ch): if ch *: return 9 if ch 0: return 0 return 1 def parse2(self, ch1, ch2): if ch1 * and ch2 *: return 15 if ch1 * and ch2 ! *: return 2 if ch2 6 else 1 if ch1 1 and ch2 *: return 9 if ch1 1 and ch2 ! *: return 1 if ch1 2 and ch2 *: return 6 if ch1 2 and ch2 ! *: return 1 if ch2 6 else 0 return 0 def numDecodings(self, s: str) - int: mod 10 ** 9 7 size len(s) dp [0 for _ in range(size 1)] dp[0] 1 dp[1] self.parse1(s[0]) for i in range(2, size 1): dp[i] dp[i - 1] * self.parse1(s[i - 1]) dp[i] dp[i - 2] * self.parse2(s[i - 2], s[i - 1]) dp[i] % mod return dp[size]4.1 parse1单字符翻译方案数parse1(ch)对应状态转移的「路径 A」只关心当前这一个字符*能扮演1 ~ 9共 9 种数字 → 返回90单独无法翻译字母表从1开始→ 返回0其余情况1 ~ 9→ 返回1。这个返回值被当作乘法系数使用dp[i] dp[i - 1] * parse1(s[i - 1])。把系数乘法与判空逻辑分离正是本题代码可读性的关键。4.2 parse2双字符翻译方案数parse2(ch1, ch2)对应「路径 B」完全复刻上文 3.3 节的 7 种情况ch1 * and ch2 *→ 两位数范围[11, 19] ∪ [21, 26]共9 6 15种ch1 * and ch2 ! *→ 十位候选只有1、2若ch2 6即[1, 6]可二者皆选得 2 种否则只有1合法得 1 种注意这里的字符比较ch2 6依赖 ASCII 排序数字字符按字典序比较与数值序一致可以安全使用ch1 1 and ch2 *→ 个位可取 9 个数字 → 9 种ch1 1 and ch2 ! *→[11, 19]全部合法 → 1 种ch1 2 and ch2 *→ 个位只能取[1, 6]→ 6 种ch1 2 and ch2 ! *→ 仅当ch2 6时落入[21, 26]否则返回 0其余ch1为3 ~ 9→ 双字符必然超过 26 → 0。上述系数与 3.3 节逐条对应可以从源码反推出结论parse2本质是对「合法两位数区间」的计数——[1, 9]与[1, 6]两个区间分别来自「10 位数字范围」与「26 的上界约束」的交集计算。4.3 主循环递推与取模numDecodings中数组dp长度为size 1下标i表示前i个字符循环从i 2开始因为i 1已被初始化每次迭代累加两个来源后立刻% mod保证dp中任意状态都不超过10^9 7从而满足题目「答案一定是 32 位整数」的数据保证循环内下标s[i - 1]、s[i - 2]与状态dp[i - 1]、dp[i - 2]一一对齐处理「前 i 个字符」时当前字符是s[i - 1]前一个字符是s[i - 2]。该实现与 0091 的解码方法 的递推骨架同构同样dp[i] dp[i-1]与dp[i] dp[i-2]双来源累加差异仅在于把「是否合法」的布尔判断升级成了「合法方案数」的系数乘法——这正是通配符把简单 DP 升级为计数型 DP 的直观体现。五、复杂度分析与空间优化时间/空间复杂度原文档结论时间复杂度O(n)。主体只有一重循环遍历字符串。空间复杂度O(n)。使用了一维数组dp保存全部状态。可以推断的优化空间观察转移式可知dp[i]只依赖dp[i - 1]与dp[i - 2]两个历史状态与更早的状态无关。因此可以把dp数组压缩为两个滚动变量如prev2、prev1每轮迭代后更新从而把空间复杂度降为O(1)时间复杂度保持不变。这与仓库在「爬楼梯」一题中对同类递推关系给出的滚动变量优化结论见 08_01 动态规划基础是一致的。六、从本题到整个线性 DP 知识体系这道题在仓库中的定位标签「字符串、动态规划」、难度「困难」决定了它是检验单串线性 DP 建模能力的进阶题若还无法流畅推导 0091 的双来源转移建议先回到 解码方法0091 打基础若想系统梳理线性 DP 的常见状态定义与题目分类可阅读 单串线性 DP 讲解 与 线性 DP 进阶在刷题清单层面本题与 0091 同属「动态规划」分类可在 分类题目列表 中按分类索引到完整练习序列在 0600-0699 题解索引 中可以看到本题与同段位其他困难题的并列关系方便按章节顺序逐题攻克。小结0639 的核心难点不在「要不要用 DP」而在「把通配符可能产生的每一种合法编码都数清楚」。通过parse1 / parse2将单字符与双字符的合法方案数拆成独立函数再以dp[i] dp[i-1] * parse1(...) dp[i-2] * parse2(...)完成递推并统一取模即可在O(n)时间内求出答案——这是「计数型线性 DP」一类题目的通用套路值得在面试中当作模板题反复记忆。赞分享教程文档知识库【免费下载链接】AlgoNote⛽️「算法通关手册」从零开始的「算法与数据结构」学习教程200 道「算法面试热门题目」1000 道「LeetCode 题目解析」持续更新中项目地址https://gitcode.com/gh_mirrors/le/AlgoNote点击查看免费下载相关推荐单串线性动态规划线性 DP完全指南状态设计、转移方程与三大经典题型精解 —— 基于 AlgoNote「算法通关手册」单串线性动态规划线性 DP完全指南状态设计、转移方程与三大经典题型精解 —— 基于 AlgoNote「算法通关手册」 线性动态规划线性 DP是动态规划教程文档知识库AlgoNote 线性 DP 进阶实战矩阵线性 DP 与无串线性 DP 解法全解AlgoNote 线性 DP 进阶实战矩阵线性 DP 与无串线性 DP 解法全解 导读 本篇是「AlgoNote 算法通关手册」 动态规划章节 https:/教程文档知识库LeetCode 0044 通配符匹配AlgoNote 动态规划解法全解析LeetCode 0044 通配符匹配AlgoNote 动态规划解法全解析 本文是《算法通关手册》AlgoNote题解体系中的一篇围绕 LeetCode教程文档知识库上一篇如何用 Caddy ForwardProxy 构建防探测代理probe_resistance 功能深度教程下一篇如何用OpenRV提升VFX工作流效率10个专业级使用技巧创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表