91. 解码方法(Decode Ways)

难度:中等 | 主题:动态规划、字符串

题目

一条包含字母 A-Z 的消息通过以下映射进行了编码:‘A’ → 1, ‘B’ → 2, …, ‘Z’ → 26。给你一个只含数字的非空字符串 s,请计算并返回解码方法的总数。

示例

 
s = "226" → 输出 3
 
解释:
 
  "BZ"  (2, 26)
 
  "VF"  (22, 6)
 
  "BBF" (2, 2, 6)
 
s = "06" → 输出 0
 
解释:"06" 不能解码,"6" 可以但 "0" 不可以
 

思路

先讲个故事:破译密码

你收到一条密文,满是数字:“1226”。

你知道密码本:1→A, 2→B, …, 26→Z。

问题来了:“12” 可以是一个整体(L)也可以是两个数字(1→A, 2→B)。

“26” 同理。

但 “0” 就不行——密码本没有 0。

所以你需要统计:给定数字字符串,有多少种解码方式?

这就是解码问题——把字符串分段,每段映射到 1~26,统计全部分割方案数。


引导式推导:从末尾一位和两位开始

第 1 步(只看最后一个字符):前 i 个字符 s[:i] 的解码方式数 = dp[i]

最后一个字符 s[i-1] 有两种处理方式:

  • 单独解码s[i-1] 作为 1 位数 → 前提是 s[i-1] != '0'

  • 组合解码s[i-2:i] 作为两位数 → 前提是数字在 10~26 之间

这两种情况互斥且覆盖所有可能,所以方式数相加

第 2 步(状态转移)

 
dp[i] = 0
 
if s[i-1] != '0':                     # 单独解码
 
    dp[i] += dp[i-1]
 
if i >= 2 and '10' <= s[i-2:i] <= '26':  # 组合解码
 
    dp[i] += dp[i-2]
 

graph TD

    subgraph SGfv0np["解码决策树 s=“226“"]

        A["dp[3] = dp[2] + dp[1]"] --> B1["单独解码 '6' -><br/>dp[2]"]

        A --> B2["组合解码 '26' -><br/>dp[1]"]

        B1 --> C1["单独解码 '2' -><br/>dp[1]"]

        B1 --> C2["组合解码 '22' -><br/>dp[0]"]

        B2 --> D1["单独解码 '2' -><br/>dp[0]"]

        C1 --> E1["dp[1] = 1('2')"]

        C2 --> E2["dp[0] = 1(空串)"]

        D1 --> E3["dp[0] = 1(空串)"]

    end

第 3 步(边界条件)

i含义原因
dp[0]空字符串1空字符串有 1 种解码方式——什么都不做
dp[1]第一个字符1(如果 s[0] != ‘0’)单个非零数字只有 1 种解码

两层递进


graph LR

    subgraph 两层递进

        A["一维 DP 数组<br/>O(n) O(n)"]

        B["滚动变量<br/>O(n) O(1)"]

    end

    A -->|只依赖前两个状态| B

一维 DPdp = [0] * (n + 1),循环 O(n) 填表。

滚动变量dp[i] 只依赖 dp[i-1]dp[i-2]——和斐波那契一样。用 prev1prev2 两个变量滚动。


代码

 
def numDecodings(self, s):
 
    if not s or s[0] == '0':          # 空串或以 0 开头 → 无法解码
 
        return 0
 
    n = len(s)
 
    prev2 = 1                          # dp[0] = 1(空字符串)
 
    prev1 = 1                          # dp[1] = 1(第一个字符非 0)
 
    for i in range(2, n + 1):
 
        curr = 0
 
        # 单独解码当前字符 s[i-1]
 
        if s[i - 1] != '0':
 
            curr += prev1              # 继承 dp[i-1] 的所有方式
 
        # 组合解码前两个字符 s[i-2:i]
 
        two_digit = int(s[i - 2:i])
 
        if 10 <= two_digit <= 26:
 
            curr += prev2              # 继承 dp[i-2] 的所有方式
 
        prev2 = prev1                  # 滚动更新
 
        prev1 = curr
 
    return prev1                       # dp[n] = 整个字符串的解码方式数
 

复杂度

指标解释
时间O(n)一次遍历字符串
空间O(1)两个滚动变量
DP 数组O(n)可提但非最优

实战考量

频率分析

出现在:字节/腾讯/美团 二面字符串 DP 题,约 15% 的同类题会考解码问题。常作为”爬楼梯”的字符串版本——都是斐波那契式递推,只是加了个”有效范围”约束。

延伸思考

Q:和 70爬楼梯 的关系?

A:本质上是一样的!70 题:f(n) = f(n-1) + f(n-2)(无条件加)。91 题:dp[i] = dp[i-1] + (如果组合有效则 + dp[i-2])——带条件的斐波那契。区别就是多了个”组合必须在 10~26 之间”的约束。

Q:‘0’ 的处理为什么特殊?

A:‘0’ 不能单独解码(密码本没有 0)。但 ‘0’ 可以和前面的 ‘1’ 或 ‘2’ 组合成 ‘10’ 或 ‘20’。所以:

  • 当前字符是 ‘0’ → 不能单独解码,+= dp[i-1] 不触发

  • 前一个字符是 ‘0’ → 组合如 ‘00’ 或 ‘30’ 不在 10~26 内,+= dp[i-2] 不触发

  • 连续的 ‘0’(如 “100”)→ 中间两位 “00” 不可解码 → 整体解码方式为 0

Q:如果要求输出所有解码方案呢?

A:改成回溯(DFS)——每步尝试取 1 位或 2 位,和 131分割回文串 的框架相同。复杂度从 O(n) 变成 O(2ⁿ),因为枚举所有方案。

Q:如果允许数字对应多个字母呢?

A:这是带通配符的变体(LeetCode 639)。'*' 可以代表 1~9,组合解码 '*' + 某个数字'1*' + '*' 时有多种对应,方案数需要相乘。常作为进阶追问。

Q:字符串很长(10⁵)怎么办?

A:O(n) 已经最优了。注意 Python 的整数不会溢出,但其他语言可能需要注意用取模(如 mod 10⁹+7)。

易错点

  • 当前字符 ‘0’ 不能单独解码(只可能和前面组合成 10 或 20)

  • 两位组合必须在 10~26 范围内(01 不行,10 可以,27 不行)

  • dp[0] = 1(空字符串有一种解码方式——什么也不做)

  • 以 ‘0’ 开头的字符串直接返回 0

  • 滚动变量时 prev2prev1 的初始值及更新顺序


生活类比

解码方法 → 破译密码 → 中文拼音

想想中文输入法的拼音到汉字的映射:“beijing”

  • 可以分成 bei-jing(北京)
  • 也可以分成 be-i-jing(无意义)
  • 还可以分成 b-e-i-j-i-n-g(一堆字母)

解码问题就是:给定一串拼音字母,有多少种合理的中文分词方式?

只不过这里的”拼音”变成了 126 的数字密码,“汉字”变成了 AZ 的字母。

而 0 就像一个无法单独发音的声母——它必须依附在前面的韵母上。


相关题目

题目关系
70爬楼梯同源递推,无条件加法 vs 带条件加法
198打家劫舍相邻决策变体
131分割回文串回溯输出所有解码方案时框架相同
639. 解码方法二(带 * 通配符)进阶版,带通配符的计数乘法
91解码方法本题,带约束的斐波那契

→ 返回题单:LeetCode学习路线图 > 十三、动态规划

速记卡(面试闪卡)

Q1:一句话讲清「91. 解码方法(Decode Ways)」到底是什么?

A:统计数字串映射成 A~Z 字母的解码方式总数,是带有效范围约束的斐波那契。

Q2:题目:破译数字密码(decode ways / A-Z mapping) —— 怎么理解?

A:收到满屏数字 “1226”,密码本 1→A…26→Z。问有多少种切分方式?“12” 可整体可拆开,但 “0” 没对应字母必须依附前面的 1 或 2——统计所有合法分割数。

Q3:思路:末位单解 + 双解(DP with 1-digit & 2-digit) —— 怎么理解?

A:第 i 位方式数 dp[i] = dp[i-1](末位单独解,且非 0)+ dp[i-2](末两位组合,且在 10~26)。两种互斥覆盖全部,所以相加——像爬楼梯,只是加了”组合有效”的闸。

Q4:代码:滚动变量(rolling prev1/prev2) —— 怎么理解?

A:prev2=1(空串)、prev1=1(首字符非0);遍历中 s[i-1]!='0' 加 prev1,10<=两位<=26 加 prev2,滚动更新。以 ‘0’ 开头直接返回 0。空间 O(1)。

Q5:复杂度与实战(O(n) time) —— 怎么理解?

A:时间 O(n)(一遍扫描),空间 O(1)。字节腾讯美团约 15% 考,常作爬楼梯的字符串版;易错:‘0’ 不能单解、组合须 10~26、dp[0]=1。进阶带 ’*’ 用乘法(639)。

Q6:核心速记主线有哪些?

  • 题目:数字串映射 A~Z 的解码方案总数

  • 思路:末位单解(dp[i-1]) + 末两位组合解(dp[i-2])

  • 代码:滚动 prev1/prev2,0 开头返回 0

  • 复杂度:时间 O(n)、空间 O(1)

  • 实战:组合须 10~26,dp[0]=1,进阶带通配符

口诀

A:数字密码如何翻,

单拆双组两路算;

零必依附前一伴,

解码总数自然现。

相关链接