#91 一维DP 中等

解码方法

在 LeetCode 上查看 ↗

🎧 语音讲解

开车或通勤时可听,跟着思路走一遍

速度

📋 题目描述

一条包含字母 A-Z 的消息通过以下映射进行了 编码:

"1" → 'A',"2" → 'B',…,"25" → 'Y',"26" → 'Z'。

然而,在 解码 已编码的消息时,有些编码被包含在其它编码当中(如 "2" 与 "5" 也可组成 "25")。

给你一个只含数字的 非空 字符串 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" 不能作为合法两位编码(前导零),整串无法解码。

💭 模拟答题者思考

1. 最直接:从左到右 DFS,每位尝试切 1 个或 2 个数字,遇到非法(如前导零、>26)回溯。分支在重叠子问题上爆炸,n=100 会超时。

2. 重复在哪里?「前 i 个字符有多少种解码」只依赖更短前缀的方案数,与具体字母路径无关——同一前缀被不同 DFS 路径反复访问。

3. 关键转化:设 f(i) = s[0..i) 的方案数。最后一刀要么只取 s[i-1](不能是单独的 '0'),要么取 s[i-2..i) 且数值在 [10,26](单 digit 的 1–9 走一位分支)。则 f(i) = f(i-1)的合法贡献 + f(i-2)的合法贡献,与 #70 爬楼梯「最后一步 1 或 2」同形,但每步要先判字符是否允许切分。

4. 手推 "226":f(0)=1;"2"→1;"22" 可 2+2 或 22→2;"226" 末位 6 单独、或 26 一起→3,与样例一致。

5. "06":首位单独 0 非法,两位 06 也非法,f(1)=0 传导使答案为 0——前导零必须在转移里挡掉,不能当普通一位数字。

🧠 变量语义(先读这三句再编码)

变量类型语义(三句法)
dp[i](或 prev2, prev1)int定义:前缀 s[0..i)(长度为 i)的合法解码方案总数
维护:dp[0]=1 表示空前缀一种方式;推到 i 时只看最后一格单独解码或最后两格一起解码
更新:若末位单字符合法则 += dp[i-1];若末两位在 10–26 则 += dp[i-2]
prev1int定义:滚动数组中当前的 dp[i-1](上一前缀的方案数)
维护:每处理完位置 i,该值将成为下一轮的「前一项」
更新:每轮先保存旧 prev1 到 prev2,再把新算出的 cur 赋给 prev1
prev2int定义:滚动数组中的 dp[i-2]
维护:与 prev1 同步前移,供「两位一组」转移使用
更新:prev2, prev1 = prev1, cur 式滚动
curint定义:正在计算的 dp[i]
维护:每轮从 0 累加:单字符分支 + 双字符分支(各自判合法)
更新:若 cur==0 且已无法延续,可提前结束;最终 prev1 即为 dp[n]

⌨️ 落码步骤

1. 若 s[0]=='0' 或空串,直接返回 0

2. 初始化 prev2=1(dp[0])、prev1=1(dp[1] 在首字符非 0 时)

3. 对 i 从 2 到 n,令 cur=0

4. 单字符:若 s[i-1]!='0',则 cur += prev1

5. 双字符:取 two = int(s[i-2:i]),若 10 <= two <= 26,则 cur += prev2

6. 滚动 prev2, prev1 = prev1, cur;若中途 cur==0 可继续(后面仍可能为 0)

7. 返回 prev1(即 dp[n])

💻 代码实现

class Solution:
    def numDecodings(self, s: str) -> int:
        if not s or s[0] == "0":
            return 0
        n = len(s)
        prev2, prev1 = 1, 1
        for i in range(2, n + 1):
            cur = 0
            if s[i - 1] != "0":
                cur += prev1
            two = int(s[i - 2 : i])
            if 10 <= two <= 26:
                cur += prev2
            prev2, prev1 = prev1, cur
        return prev1
class Solution {
public:
    int numDecodings(string s) {
        if (s.empty() || s[0] == '0') return 0;
        int n = s.size();
        long prev2 = 1, prev1 = 1;
        for (int i = 2; i <= n; ++i) {
            long cur = 0;
            if (s[i - 1] != '0') cur += prev1;
            int two = (s[i - 2] - '0') * 10 + (s[i - 1] - '0');
            if (two >= 10 && two <= 26) cur += prev2;
            prev2 = prev1;
            prev1 = cur;
        }
        return (int)prev1;
    }
};
// 时间 O(n),空间 O(1)

📈 复杂度分析

时间复杂度 O(n)(n 为字符串长度,每位至多看一位与两位切分)
空间复杂度 O(1)(滚动两个状态;数组版 dp 为 O(n))

⚠️ 常见坑

❗ 忽略前导零:两位编码必须是 10–26;"06"、"01" 不能走双字符分支,且单独的 '0' 不能走单字符分支,否则会把非法串算成有解。

❗ 与爬楼梯混淆:不是无脑 dp[i]=dp[i-1]+dp[i-2],每一位转移前都要判当前切分是否对应 1–26 的合法映射。

❗ 初始化 dp[1]:仅当 s[0]!='0' 时为 1;若首字符为 0,整个答案为 0,不要默认 dp[1]=1。

🔍 必测边界 Case

Case 1:示例 1
s = "12" → 2
Case 2:前导零
s = "06" → 0
Case 3:含 10/20 边界
s = "10" → 1(只能 10,不能 1+0)
Case 4:单字符
s = "8" → 1
Case 5:全串 0
s = "0" → 0