Problem overview
The method numDecodings receives a string s of digits. A message of capital letters was turned into digits by writing each letter as its position in the alphabet: A is 1, B is 2, up to Z as 26. To decode, split the digits into groups, each group a number from 1 to 26, and turn each group back into a letter. A group can't start with 0: "06" is not "6", and no letter is 0. The method returns how many different ways the string can be decoded, or 0 when it can't be decoded at all.
For "12" the answer is 2: the groups 1, 2 give "AB" and the group 12 gives "L". For "226" the answer is 3: 2, 26 gives "BZ", 22, 6 gives "VF", and 2, 2, 6 gives "BBF". For "06" the answer is 0, because the first group would have to start with 0.
Approach: count the ways for each prefix
Trying every split is a recursion that branches at each position, and it repeats the same sub-problems many times. The better idea is to count decodings of every beginning of the string, from the shortest to the longest.
Let dp[i] be the number of ways to decode the first i digits. Look at the end of those i digits. The last group is either one digit or two:
- One digit: the last digit
s[i - 1]is a letter on its own when it is 1 to 9. Then every decoding of the firsti - 1digits, followed by that letter, is a decoding of the firstidigits. This addsdp[i - 1]. - Two digits: the last two digits form a letter when they make a number from 10 to 26. Then every decoding of the first
i - 2digits, followed by that letter, counts. This addsdp[i - 2].
The two cases can both apply, and they never produce the same decoding, because the last group has a different length. If neither applies, dp[i] is 0, and since later entries are built from earlier ones, a block that can't be decoded makes the counts built on it 0 as well. The table needs a starting point: dp[0] is 1 for the empty beginning, which has one decoding with no letters, and that value is what every longer decoding is built on.
Here is the table for a few inputs, as the code fills it. The first entry is dp[0]:
"12" 1 1 2
"226" 1 1 2 3
"11106" 1 1 2 3 2 2
"06" 1 0 0
For "226": dp[1] is 1 (just "2"). For dp[2], the last digit 2 is a letter, adding dp[1], and 22 is a letter, adding dp[0], so it is 2. For dp[3], the last digit 6 adds dp[2] and 26 adds dp[1], so 2 + 1 = 3. The final entry, 3, is the answer. For "11106", the 0 can't stand alone, so dp[4] only gets the two-digit group 10, and dp[5] only gets the single 6 added to dp[4], because 06 is not a letter. The answer is 2.
Step by step
- Store the length of
sinint n = s.size(). - Create the table.
vector<int> dp(n + 1, 0)has one more entry than there are digits, becausedp[0]stands for the empty beginning. - Set
dp[0] = 1. The empty beginning counts as one way, not zero, or every longer count would stay 0. - Handle the first digit. If
s[0] != '0', setdp[1] = 1. A single digit 1 to 9 is a letter. A string that starts with 0 leavesdp[1]at 0, and every count built on it stays 0. - Loop with
ifrom 2 whilei <= n. The first two entries are known, each later entry needs the two before it, and the loop ends withdp[n], for the whole string. - Add the one-digit case. If
s[i - 1] != '0', dodp[i] += dp[i - 1]. - Read the last two digits as a number.
int two = stoi(s.substr(i - 2, 2)). The callsubstr(i - 2, 2)takes two characters starting at positioni - 2. - Add the two-digit case. If
two >= 10 && two <= 26, dodp[i] += dp[i - 2]. A pair below 10 starts with 0 (like 07) and is no letter, and neither is 27 or more. - Return
dp[n]after the loop.
The solution in C++
class Solution {
public:
int numDecodings(string s) {
int n = s.size();
vector<int> dp(n + 1, 0);
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 = stoi(s.substr(i - 2, 2));
if (two >= 10 && two <= 26) {
dp[i] += dp[i - 2];
}
}
return dp[n];
}
};
The C++ details worth a closer look:
- Characters and digits.
s[i - 1]is achar, so it is compared with the character'0'in single quotes, not the number 0. The checks[i - 1] != '0'is true for the digits'1'to'9'. substrandstoi.s.substr(pos, len)returns a new string of up tolencharacters starting atpos.stoiconverts it to anint, and leading zeros are fine:stoi("06")is 6. That is why the range testtwo >= 10is needed, since "06" would otherwise pass as a letter.- The vector constructor.
vector<int> dp(n + 1, 0)makesn + 1elements, all 0. Writingvector<int> dp{n + 1, 0}with braces would instead make a vector of two elements. - Passing the string by value.
numDecodings(string s)receives a copy. That is cheap here (at most 100 characters), and the method doesn't change it. - Size and the first digit. The lesson guarantees at least one character. Reading
s[0]and writingdp[1]on an empty string would go outside the vector, which is undefined behaviour. - Overflow. The counts are
int. The statement promises that the answer fits in 32 bits, so no wider type is needed. In general, a string like"1111111111"grows the count like the Fibonacci numbers, and a very long input would overflow a signedint, which is undefined behaviour.
Complexity
Time: O(n). The loop runs n - 1 times and each pass does a constant amount of work: two comparisons, one two-character substr and one stoi.
Space: O(n). The table has n + 1 integers. Because each entry uses only the previous two, two variables could replace the table and bring the space down to O(1), but the table is easier to read.
Edge cases and common mistakes
- A leading zero.
"06"and"0"give 0. No letter is 0 and a group can't start with it, sodp[1]stays 0. - A zero in the middle. A 0 can only be the second digit of 10 or 20.
"10"gives 1 (only "J"), while"100"gives 0, since the last 0 has no digit to pair with."30"gives 0 as well, because 30 is too large and 0 is no letter. - A single digit.
"7"gives 1. The loop doesn't run, anddp[1]is already set. - Numbers above 26.
"27"gives 1: the two digits are not a letter, but 2 and 7 are. The testtwo <= 26matters. - Zeros after other digits.
"2101"gives 1: the only valid split is 2, 10, 1. - Starting
dp[0]at 0. Every count is built from it, so the whole table would stay 0. - Using only the two-digit number. Skipping the check of the last single digit counts "06" as the letter 6, and gives wrong answers for strings with zeros.
- An off-by-one on
substr. The two digits ending at theith digit start at indexi - 2, noti - 1.
Testing
s = "12"gives2.s = "226"gives3.s = "06"gives0.s = "11106"gives2.s = "10"gives1.s = "100"gives0.s = "0"gives0.s = "7"gives1.s = "27"gives1.s = "2101"gives1.s = "30"gives0.
