Problem overview
NumDecodings receives a string s of digits and returns how many ways it can be read as letters, where A is 1, B is 2, and so on up to Z as 26. A reading splits the digits into groups, each a number from 1 to 26, and no group may start with 0: "06" is not 6, and no letter is 0. If there is no valid reading, the answer is 0.
For "12" the answer is 2: AB (1, 2) or L (12). For "226" it is 3: BZ (2, 26), VF (22, 6) or BBF (2, 2, 6). For "06" it is 0, since the first group would have to start with 0.
Approach: count the readings of each beginning
Let dp[i] be the number of ways to decode the first i digits of s. The last group of a reading of those i digits is either one digit or two digits, and nothing else:
- If the last digit alone is a letter (it is not
'0'), then every reading of the firsti - 1digits can be extended by that letter. That addsdp[i - 1]ways. - If the last two digits form a number from 10 to 26, they are one letter, and every reading of the first
i - 2digits can be extended by it. That addsdp[i - 2]ways.
The two cases never produce the same reading, because the last group has a different length. So dp[i] is the sum of the cases that apply, and each entry takes constant work: O(n) time overall. A plain recursion that tries both group sizes at each position repeats the same suffixes many times; the table computes each one once.
Step by step
- Store the length:
int n = s.Length. - Create the table:
int[] dp = new int[n + 1]. It has one more entry than there are digits, becausedp[0]stands for the empty beginning. C# fills a newintarray with zeros. dp[0] = 1. The empty beginning has exactly one decoding, the empty message. Every longer decoding is built on it, so it counts as one way, not zero.- First digit: if
s[0] != '0', setdp[1] = 1. A single digit 1 to 9 is a letter. If the string starts with 0,dp[1]stays 0, and every count built on it stays 0 as well. - Loop
ifrom 2 whilei <= n. The entries 0 and 1 are known, and the last one,dp[n], covers all ofs. - One-digit group: if
s[i - 1] != '0', dodp[i] += dp[i - 1]. - Two-digit group: take
s.Substring(i - 2, 2), turn it into anintwithint.Parse, and keep it intwo. - Valid pair: if
two >= 10 && two <= 26, dodp[i] += dp[i - 2]. A pair like07parses to 7 and is rejected because it is below 10; a pair above 26 is no letter either. - Return
dp[n].
The solution in C#
class Solution {
public int NumDecodings(string s) {
int n = s.Length;
int[] dp = new int[n + 1];
dp[0] = 1;
if (s[0] != '0') {
dp[1] = 1;
}
for (int i = 2; i <= n; i++) {
if (s[i - 1] != '0') {
dp[i] += dp[i - 1];
}
int two = int.Parse(s.Substring(i - 2, 2));
if (two >= 10 && two <= 26) {
dp[i] += dp[i - 2];
}
}
return dp[n];
}
}
C# details worth knowing:
s[i]is achar, compared with a character literal in single quotes:s[0] != '0'. Comparing it with the string"0"would not compile. Acharis not the number it shows:s[0] - '0'gives the digit's value as anint, while(int)s[0]would give the character code, 48 for'0'.Substring(start, length)takes a start index and a length, not an end index:"226".Substring(1, 2)is"26". The second argument is how many characters to take.int.Parseaccepts leading zeros:int.Parse("07")is 7. That is why the range check starts at 10, which excludes pairs that begin with 0. The input contains only digits, soParsenever throws here.- Strings are immutable.
Substringcreates a new string each time it is called, and nothing changes ins. That small allocation per position is what the two-digit step costs; computing(s[i - 2] - '0') * 10 + (s[i - 1] - '0')avoids it. +=on an array element works like on any variable:dp[i] += dp[i - 1]adds to the entry in place.- The result is an
int. The lesson guarantees the answer fits in 32 bits. In general,intoverflow wraps silently, so a longer string could give a wrong number;longwould be needed for larger answers.
Complexity
Time: O(n). The loop runs n - 1 times and does constant work per step: one comparison, one parse of two characters and at most two additions.
Space: O(n) for the dp table. Each entry needs only the two before it, so two variables could replace the table and reduce the space to O(1).
Edge cases and common mistakes
- A leading zero.
"06"returns 0 and"0"returns 0. - A zero that can't be paired.
"100"returns 0: the last0would have to join the previous0as00, which is no letter. And"30"returns 0, because 30 is above 26 and the 0 can't stand alone. - A zero that can be paired.
"10"returns 1, onlyJ. The one-digit step is skipped for the 0, and the two-digit step addsdp[0]. - Numbers just above 26.
"27"returns 1 (only 2 and 7), because 27 is not a letter. - One digit.
"1"returns 1. Forn = 1the loop doesn't run, and the code relies ondp[1]having been set before it. - Forgetting
dp[0] = 1. Every two-digit case would add 0 where it should add 1, and"12"would give 1 instead of 2. - Accepting
"07"as 7. That mistake comes from checking onlytwo <= 26. The lower bound of 10 is what rules it out.
Testing
Running NumDecodings and printing the dp table next to each result gives:
s = "12" dp = 1 1 2 -> 2
s = "226" dp = 1 1 2 3 -> 3
s = "06" dp = 1 0 0 -> 0
s = "11106" dp = 1 1 2 3 2 2 -> 2
s = "10" dp = 1 1 1 -> 1
s = "27" dp = 1 1 1 -> 1
s = "100" dp = 1 1 1 0 -> 0
s = "30" dp = 1 1 0 -> 0
s = "2101" dp = 1 1 2 1 1 -> 1
In the table for "226", dp[2] is 2 because "22" reads as 2, 2 or as 22. For dp[3], the last digit 6 is a letter on its own and adds dp[2], which is 2. The pair "26" is also a letter and adds dp[1], which is 1. Together: 2 + 1 = 3.
