Number of Ways to Decode a Message
Prereq: Memoization Intro
We have a message consisting of digits 0-9
to decode. Letters are encoded to digits by their positions in the alphabet
1A -> 1 2B -> 2 3C -> 3 4... 5Y -> 25 6Z -> 26
Given a non-empty string of digits, how many ways are there to decode it?
Input: "18"
Output: 2
Explanation: "18"
can be decoded as "AH"
or "R"
Input: "123"
Output: 3
Explanation: "123"
can be decoded as "ABC"
, "LC"
, "AW"
Try it yourself
Solution
We can start from the beginning of the string and try to decode each digit in the string until we get to the end of the string.
Each digit 1-9
maps to an alphabet.
For digits 1
and 2
there is a possibility to decode two consecutive digits together.
For example, there are two ways to decode 12
:
1 => A
and2 => B
12 => L
.
There are two ways to decode 26
:
2 => B
,6 => F
26 => Z
.
There is only one way to decode 27
2 => B and 7 => G
because there is no 27th alphabet.
It's impossible to decode a string with a leading 0
such as 02
because 0
does not map to any alphabet.
The only way to decode a string with 0
is to have a preceeding 1
or 2
and decode as 10
and 20
respectively.
So depending on the current and following digit, there could be zero to two ways to branch out. Here is how the state-space tree looks like:
Impementation
1function dfs(start_index, [...additional states]):
2 if is_leaf(start_index):
3 return 1
4 ans = initial_value
5 for edge in get_edges(start_index, [...additional states]):
6 if additional states:
7 update([...additional states])
8 ans = aggregate(ans, dfs(start_index + len(edge), [...additional states]))
9 if additional states:
10 revert([...additional states])
11 return ans
12
1private static int dfs(Integer startIndex, List<T> target) {
2 if (isLeaf(startIndex)) {
3 return 1;
4 }
5
6 ans = initialValue;
7 for (T edge : getEdges(startIndex, [...additional states])) {
8 if (additional states) {
9 update([...additional states]);
10 }
11 ans = aggregate(ans, dfs(startIndex + edge.length(), [...additional states])
12 if (additional states) {
13 revert([...additional states]);
14 }
15 }
16 return ans;
17}
18
1function dfs(startIndex, target) {
2 if (isLeaf(startIndex)) {
3 return 1
4 }
5 int ans = initialValue;
6 for (const edge of getEdges(startIndex, [...additional states])) {
7 if (additional states) {
8 update([...additional states]);
9 }
10 ans = aggregate(ans, dfs(startIndex + edge.length(), [...additional states])
11 if (additional states) {
12 revert([...additional states]);
13 }
14 }
15 return ans;
16}
17
1int dfs(int startIndex, std::vector<T>& target) {
2 if (isValid(target[startIndex:])) {
3 return 1;
4 }
5 int counter = 0;
6 for (auto edge : getEdges(startIndex, [...additional states])) {
7 if (additional states) {
8 update([...additional states]);
9 }
10 ans = aggregate(ans, dfs(startIndex + edge.length(), [...additional states])
11 if (additional states) {
12 revert([...additional states]);
13 }
14 }
15 return ans;
16}
17
1public static int Dfs(int startIndex, List<T> target)
2{
3 if (IsLeaf(startIndex))
4 {
5 return 1;
6 }
7 int ans = initialValue;
8 foreach (T edge : getEdges(startIndex, [...additional states]))
9 {
10 if (additional states) {
11 update([...additional states]);
12 }
13 ans = aggregate(ans, dfs(startIndex + edge.length(), [...additional states])
14 if (additional states) {
15 revert([...additional states]);
16 }
17 }
18 return ans;
19}
20
And fill out the missing logic:
is_leaf
: ifstart_index
reaches thedigits.length
then we have matched every digit and the decoding is done.get_edges
: we usestartIndex
to record the next digit to match. We can always match one digit first. If there are two consecutive digits that falls in10-26
then we can match two digits.initial_value
: we start with0
because we haven't matched anything.aggregate
: we add the number of ways to decode from the subtree.addtional states
: there is no additional states; we have all the information to determine how to branch out.
1def decode_ways(digits):
2 def dfs(start_index):
3 if start_index == len(digits):
4 return 1
5
6 ways = 0
7 # can't decode string with leading 0
8 if digits[start_index] == '0':
9 return ways
10 # decode one digit
11 ways += dfs(start_index + 1)
12 # decode two digits
13 if 10 <= int(digits[start_index: start_index + 2]) <= 26:
14 ways += dfs(start_index + 2)
15
16 return ways
17
18 return dfs(0)
19
1private static int dfs(int startIndex, String digits) {
2 if (startIndex == digits.length()) return 1;
3
4 int ways = 0;
5 // can't decode string with leading 0
6 if (digits.charAt(startIndex) == '0') {
7 return ways;
8 }
9 // decode one digit
10 ways += dfs(startIndex + 1, digits);
11 // decode two digits
12 if (startIndex + 2 <= digits.length() && Integer.parseInt(digits.substring(startIndex, startIndex + 2)) <= 26) {
13 ways += dfs(startIndex + 2, digits);
14 }
15
16 return ways;
17}
18
19public static int decodeWays(String digits) {
20 return dfs(0, digits);
21}
22
1function decodeWays(digits) {
2 return dfs(0, digits);
3}
4
5function dfs(startIndex, digits) {
6 if (startIndex === digits.length) return 1;
7
8 let ways = 0;
9 // can't decode string with leading 0
10 if (digits[startIndex] == "0") {
11 return ways;
12 }
13 // decode one digit
14 ways += dfs(startIndex + 1, digits);
15 // decode two digits
16 if (startIndex + 2 <= digits.length && parseInt(digits.substring(startIndex, startIndex + 2)) <= 26) {
17 ways += dfs(startIndex + 2, digits);
18 }
19
20 return ways;
21}
22
1int dfs(int startIndex, std::string digits) {
2 if (startIndex == digits.length()) return 1;
3
4 int ways = 0;
5 // can't decode string with leading 0
6 if (digits[startIndex] == '0') {
7 return ways;
8 }
9 // decode one digit
10 ways += dfs(startIndex + 1, digits);
11 // decode two digits
12 if (startIndex + 2 <= digits.length() && stoi(digits.substr(startIndex, 2)) <= 26) {
13 ways += dfs(startIndex + 2, digits);
14 }
15 return ways;
16}
17
18int decode_ways(std::string digits) {
19 return dfs(0, digits);
20}
21
1private static int Dfs(int startIndex, string digits)
2{
3 if (startIndex == digits.Length) return 1;
4 int ways = 0;
5 // can't decode string with leading 0
6 if (digits[startIndex] == '0') {
7 return ways;
8 }
9 // decode one digit
10 ways += Dfs(startIndex + 1, digits);
11 // decode two digits
12 if (startIndex + 2 <= digits.Length && int.Parse(digits.Substring(startIndex, 2)) <= 26) {
13 ways += Dfs(startIndex + 2, digits);
14 }
15 return ways;
16}
17
18public static int DecodeWays(string digits)
19{
20 return Dfs(0, digits);
21}
22
Time Complexity
In the worse case, every digit can decode can be decoded in two ways. With n
digits, there are O(2^n)
nodes in the state-space tree.
We do O(1)
operation for each node so the overall time complexity is O(2^n)
.
Memoization
Similar to the previous problem, we see there are duplicated subtrees.
The green subtree and the red subtree contains the exact same content 3
and had the same prefix 12
.
The green subtree is visited before the red subtree and we can memoize the results from green subtree by keeping a memo
array that records the start_index
of the remaining strings to be decoded.
1def decode_ways(digits):
2 memo = {}
3 def dfs(start_index, memo):
4 if start_index in memo:
5 return memo[start_index]
6 if start_index == len(digits):
7 return 1
8
9 ways = 0
10 # can't decode string with leading 0
11 if digits[start_index] == '0':
12 return ways
13 # decode one digit
14 ways += dfs(start_index + 1, memo)
15 # decode two digits
16 if 10 <= int(digits[start_index: start_index + 2]) <= 26:
17 ways += dfs(start_index + 2, memo)
18
19 memo[start_index] = ways
20 return ways
21
22 return dfs(0, memo)
23
24if __name__ == '__main__':
25 digits = input()
26 res = decode_ways(digits)
27 print(res)
28
1import java.util.Arrays;
2import java.util.List;
3import java.util.Scanner;
4import java.util.stream.Collectors;
5import java.util.stream.IntStream;
6
7class Solution {
8 private static int dfs(int startIndex, String digits, int[] memo) {
9 if (startIndex == digits.length()) return 1;
10 if (memo[startIndex] != -1) return memo[startIndex];
11
12 int ways = 0;
13 // can't decode string with leading 0
14 if (digits.charAt(startIndex) == '0') {
15 return ways;
16 }
17 // decode one digit
18 ways += dfs(startIndex + 1, digits, memo);
19 // decode two digits
20 if (startIndex + 2 <= digits.length() && Integer.parseInt(digits.substring(startIndex, startIndex + 2)) <= 26) {
21 ways += dfs(startIndex + 2, digits, memo);
22 }
23 memo[startIndex] = ways;
24
25 return ways;
26 }
27
28 public static int decodeWays(String digits) {
29 int[] memo = new int[digits.length()];
30 Arrays.fill(memo, -1);
31 return dfs(0, digits, memo);
32 }
33
34 public static void main(String[] args) {
35 Scanner scanner = new Scanner(System.in);
36 String digits = scanner.nextLine();
37 scanner.close();
38 int res = decodeWays(digits);
39 System.out.println(res);
40 }
41}
42
1using System;
2
3class Solution
4{
5 private static int Dfs(int startIndex, string digits, int[] memo)
6 {
7 if (startIndex == digits.Length) return 1;
8 if (memo[startIndex] != -1) return memo[startIndex];
9 int ways = 0;
10 // can't decode string with leading 0
11 if (digits[startIndex] == '0') {
12 return ways;
13 }
14 // decode one digit
15 ways += Dfs(startIndex + 1, digits, memo);
16 // decode two digits
17 if (startIndex + 2 <= digits.Length && int.Parse(digits.Substring(startIndex, 2)) <= 26) {
18 ways += Dfs(startIndex + 2, digits, memo);
19 }
20 memo[startIndex] = ways;
21 return ways;
22 }
23
24 public static int DecodeWays(string digits)
25 {
26 int[] memo = new int[digits.Length];
27 Array.Fill(memo, -1);
28 return Dfs(0, digits, memo);
29 }
30
31 public static void Main()
32 {
33 string digits = Console.ReadLine();
34 int res = DecodeWays(digits);
35 Console.WriteLine(res);
36 }
37}
38
1function decodeWays(digits) {
2 let memo = {};
3 return dfs(0, digits, memo);
4}
5
6function dfs(startIndex, digits, memo) {
7 if (startIndex === digits.length) return 1;
8 if (startIndex in memo) return memo[startIndex];
9
10 let ways = 0;
11 // can't decode string with leading 0
12 if (digits[startIndex] == "0") {
13 return ways;
14 }
15 // decode one digit
16 ways += dfs(startIndex + 1, digits, memo);
17 // decode two digits
18 if (startIndex + 2 <= digits.length && parseInt(digits.substring(startIndex, startIndex + 2)) <= 26) {
19 ways += dfs(startIndex + 2, digits, memo);
20 }
21 memo[startIndex] = ways;
22
23 return ways;
24}
25
26function* main() {
27 const digits = yield;
28 const res = decodeWays(digits);
29 console.log(res);
30}
31
32class EOFError extends Error {}
33{
34 const gen = main();
35 const next = (line) => gen.next(line).done && process.exit();
36 let buf = '';
37 next();
38 process.stdin.setEncoding('utf8');
39 process.stdin.on('data', (data) => {
40 const lines = (buf + data).split('\n');
41 buf = lines.pop();
42 lines.forEach(next);
43 });
44 process.stdin.on('end', () => {
45 buf && next(buf);
46 gen.throw(new EOFError());
47 });
48}
49
1#include <iostream> // cin, cout
2#include <string> // getline
3
4int dfs(int startIndex, std::string digits, int memo[]) {
5 if (startIndex == digits.length()) return 1;
6 if (memo[startIndex] != -1) return memo[startIndex];
7
8 int ways = 0;
9 // can't decode string with leading 0
10 if (digits[startIndex] == '0') {
11 return ways;
12 }
13 // decode one digit
14 ways += dfs(startIndex + 1, digits, memo);
15 // decode two digits
16 if (startIndex + 2 <= digits.length() && stoi(digits.substr(startIndex, 2)) <= 26) {
17 ways += dfs(startIndex + 2, digits, memo);
18 }
19 memo[startIndex] = ways;
20 return ways;
21}
22
23int decode_ways(std::string digits) {
24 int memo[digits.length()];
25 std::fill_n(memo, digits.length(), -1);
26 return dfs(0, digits, memo);
27}
28
29int main() {
30 std::string digits;
31 std::getline(std::cin, digits);
32 int res = decode_ways(digits);
33 std::cout << res << '\n';
34}
35
Time complexity
The time complexity of the memoization solution is the size of the memo
array O(n
) multiplied by the number of operations per state which is O(1)
.
So the overall time complexity is O(n)
.
Space complexity
The height of the state-space tree is at most O(n)
. The size of the memo
array is O(n)
. Therefore the space complexity is O(n)
.
Got a question?ย Ask the Teaching Assistantย anything you don't understand.
Still not clear? Ask in the Forum, ย Discordย orย Submitย the part you don't understand to our editors.