Problem overview
numDecodings receives a string s of digits and returns how many ways it can be read as a message of capital letters, where A is 1, B is 2, up to Z as 26. Decoding means splitting the digits into groups, each a number from 1 to 26, and turning each group into its letter. A group can't start with 0: no letter is 0, and "06" is not the same as "6". If no split works, the answer is 0.
For "12" the answer is 2: 1, 2 gives "AB" and 12 gives "L". For "226" it is 3: 2, 26, 22, 6 and 2, 2, 6. For "06" it is 0, because the first group would have to start with 0.
The statement asks for O(n) time, where n is the length of s.
Approach: count decodings of each beginning
Trying every split directly would explore a tree of choices that doubles at almost every digit. The better view is to count the decodings of each beginning of the string and build longer beginnings from shorter ones.
Let dp[i] be the number of ways to decode the first i digits. Consider how a decoding of those i digits can end. Its last group is either one digit or two:
- One digit,
s[i - 1]. It is a letter if it isn't'0'. Every decoding of the firsti - 1digits, followed by this letter, is a decoding of the firstidigits. That addsdp[i - 1]. - Two digits,
s[i - 2]ands[i - 1]. They form a letter if their value is between 10 and 26. A pair like07starts with 0 and is not allowed, and 27 or more is not a letter. That addsdp[i - 2].
These two cases never produce the same decoding, so dp[i] is their sum. Each entry takes constant work, which gives O(n) overall.
Step by step
- The length. Store
s.lengthinconst n. - The table.
dpis anumber[]ofn + 1zeros, created withnew Array(n + 1).fill(0). It has one more entry than there are digits, becausedp[0]stands for the empty beginning. - The empty beginning. Set
dp[0] = 1. The empty string has exactly one decoding, the empty message, and every longer decoding builds on it, so it must count as one. - The first digit. If
s[0] !== '0', setdp[1] = 1. A single digit from 1 to 9 is one letter. If the string starts with 0,dp[1]stays 0, and so does everything built on it. - The loop.
igoes from 2 toninclusive, because the first two entries are already known and the last one,dp[n], covers all ofs. - One-digit ending. If
s[i - 1] !== '0', adddp[i - 1]todp[i]with+=. - Read the pair.
s.substring(i - 2, i)holds the characters at positionsi - 2andi - 1. Convert it withNumberintoconst two. - Two-digit ending. If
two >= 10 && two <= 26, adddp[i - 2]todp[i]. - Answer. Return
dp[n].
The solution in TypeScript
class Solution {
numDecodings(s: string): number {
const n = s.length;
const dp: number[] = new Array(n + 1).fill(0);
dp[0] = 1;
if (s[0] !== '0') {
dp[1] = 1;
}
for (let i = 2; i <= n; i++) {
if (s[i - 1] !== '0') {
dp[i] += dp[i - 1];
}
const two = Number(s.substring(i - 2, i));
if (two >= 10 && two <= 26) {
dp[i] += dp[i - 2];
}
}
return dp[n];
}
}
TypeScript details
new Array(n + 1).fill(0)creates an array ofn + 1slots and fills each with 0. The annotationnumber[]tells the compiler what the array holds; it disappears when the code compiles to JavaScript.const dpmeans the variable always refers to the same array. The entries inside can still change, which is whatdp[i] += ...does.s[i - 1]is a one-character string, so it is compared with the string'0', not the number 0. With!==a number and a string are never equal.Number("07")is 7, so the range checktwo >= 10is what rejects pairs that start with 0.Number("26")is 26.numberis a double. Counts stay exact up to 2^53, and the statement guarantees the answer fits in 32 bits.
Tracing examples
Printing the table the solution builds gives these values:
s | dp | Answer |
|---|---|---|
"226" | [1, 1, 2, 3] | 3 |
"11106" | [1, 1, 2, 3, 2, 2] | 2 |
"100" | [1, 1, 1, 0] | 0 |
In "226", dp[2] is 2: the digit 2 on its own after "2" gives one way, and the pair 22 gives another. dp[3] adds dp[2] for the 6 alone and dp[1] for the pair 26, which is 2 + 1 = 3.
In "100", the second 0 can't stand alone, and 00 is no letter, so dp[3] is 0. In "11106", the 0 can only pair with the 1 before it as 10, which is why the count drops from 3 to 2 at that point and stays 2 after the 6.
Complexity
Time: O(n). The loop runs n - 1 times with a constant amount of work each.
Space: O(n) for the table. Each entry only needs the two before it, so two variables would be enough, but the table keeps the code easy to follow.
Edge cases and common mistakes
- Strings that start with 0.
"06"returns 0, and so does"0".dp[1]stays 0, and the pair06is rejected because 6 is below 10. - A 0 that can't be paired.
"30"and"100"return 0:30is above 26 and00is not a letter, so the 0 has nowhere to go."10"returns 1, since10is the letter J. - Pairs above 26.
"27"returns 1: only 2 and 7 separately. - One digit.
"1"and"9"return 1. The loop doesn't run, anddp[1], set before it, is returned. - Forgetting
dp[0] = 1. Every two-digit case reaches back todp[0]when the pair is at the very start, so with 0 there"12"would give 1 instead of 2. - Accepting a leading zero in a pair. Without the
two >= 10test,07would pass as 7.
Testing
Running numDecodings on small inputs gives:
s | Result |
|---|---|
"12" | 2 |
"226" | 3 |
"06" | 0 |
"11106" | 2 |
"10" | 1 |
"0" | 0 |
"27" | 1 |
"30" | 0 |
"100" | 0 |
"2101" | 1 |
"1" | 1 |
