Skip to content

4.23.7 — Decode Ways

LeetCode 91 · Medium · ★ Blind 75

The problem

A is 1, B is 2, up to Z which is 26. Given a string of digits, how many ways can it be decoded?

"12"    →  2     ("AB" = 1,2   or  "L" = 12)
"226"   →  3     ("BZ", "VF", "BBF")
"06"    →  0     (no letter is 0, and "06" is not a valid 6)

The pattern

Same shape as 4.23.1 Climbing Stairs — at each position you consume one digit or two — but now each move has a condition.

Let dp[i] be the number of ways to decode the first i characters.

dp[i] = \underbrace{dp[i-1] \ \text{if the last digit is 1–9}}_{\text{take one digit}} \ + \ \underbrace{dp[i-2] \ \text{if the last two form 10–26}}_{\text{take two digits}}

Counting, so the operator is +.

The three rules that decide everything

The whole problem is the validity conditions, and each has an input designed to catch you.

A single digit is valid unless it is 0. There is no letter 0, so "0" alone decodes zero ways.

A pair is valid only between 10 and 26. That means the first digit must be 1 or 2, and if it is 2 the second must be at most 6.

A leading zero in a pair is invalid. "06" is not 6 — the pair must start with 1 or 2, so 0 never begins a valid two-digit code. This is the case that breaks naive solutions, and "06" is on the test set for exactly that reason.

The consequence worth stating: a 0 in the input can only ever be decoded as part of 10 or 20. Any other 0 makes the whole string undecodable, and the count collapses to zero and stays there.

The solution

python
class Solution:
    def numDecodings(self, s: str) -> int:
        if not s or s[0] == '0':
            return 0

        two_back, one_back = 1, 1            # dp[0] = 1 (empty), dp[1] = 1

        for i in range(1, len(s)):
            current = 0

            if s[i] != '0':                  # take one digit
                current += one_back

            two_digit = int(s[i - 1: i + 1])
            if 10 <= two_digit <= 26:        # take two digits
                current += two_back

            two_back, one_back = one_back, current

        return one_back
ts
function numDecodings(s: string): number {
  if (!s || s[0] === '0') return 0;

  let twoBack = 1, oneBack = 1;

  for (let i = 1; i < s.length; i++) {
    let current = 0;

    if (s[i] !== '0') current += oneBack;

    const twoDigit = Number(s.slice(i - 1, i + 1));
    if (twoDigit >= 10 && twoDigit <= 26) current += twoBack;

    twoBack = oneBack;
    oneBack = current;
  }

  return oneBack;
}

dp[0] = 1 is the base case that trips people up. The empty string has exactly one decoding — the empty one. It looks like it should be 0, but making it 1 is what allows a valid two-digit code at the very start to be counted: for "12", the pair 12 contributes dp[0] = 1, which is correct.

The s[0] == '0' guard rejects a string that cannot start at all.

current starts at 0 every iteration, so a position where neither move is legal correctly yields 0 — and from then on every later position sees a 0 and stays 0. The zero propagates by itself, which is exactly right.

10 <= two_digit <= 26 covers the leading-zero rule for free, because "06" is 6, which is below 10.

Trace

"226"

icharone-digit?two-digitvalid pair?current
starttwo_back = 1, one_back = 1
12yes → +122yes → +12
26yes → +226yes → +13

Answer 3 ✓ — "BBF", "VF", "BZ".

Now "06": the guard fires on s[0] == '0' and returns 0 immediately.

And "106": at i = 1 the char is 0, so no one-digit move; the pair 10 is valid, so current = dp[0] = 1. At i = 2 the char is 6, so current = dp[1] = 1; the pair 06 is invalid. Answer 1 — "JF" ✓.

Complexity

O(n) time, O(1) space.

Where this goes next

  • Decode Ways II — the input may contain *, meaning any digit from 1 to 9. The same recurrence with counted possibilities instead of yes-or-no, and the arithmetic becomes fiddly. A good demonstration that the shape survives while the conditions get harder.
  • Climbing Stairs — this recurrence with no conditions at all. 4.23.1.
  • Word Break — the same "can I split this string" idea, but the pieces can be any length and are checked against a dictionary. 4.23.10.

The family: split a string into valid pieces. Fixed piece lengths give this problem; arbitrary lengths give Word Break; palindromic pieces give Palindrome Partitioning. Same skeleton, different validity test.

What the interviewer will push on

"Why is dp[0] = 1?" The empty string has one decoding, and that base value is what lets a leading two-digit code be counted.

"What does '06' do?" Zero ways. The pair rule requires 10 to 26, so a leading zero can never form a valid pair.

"Where can a 0 legally appear?" Only as the second digit of 10 or 20.

"Walk through '2101'." A good test — the answer is 1, and it exercises both zero rules.

"O(1) space?" Two variables, since only the previous two values are read.

One thing to volunteer: enumerate the three validity rules before writing code, and name the input that breaks each. This problem is not hard once the rules are written down; it is hard when you discover them one failing test at a time.

Next: 4.23.8 Coin Change — the classic optimisation DP, and the clearest proof that greedy fails.