Builds on Subsets, which you haven’t finished yet.
Cartesian Product Backtracking (One Choice per Slot)
start and no used array.Given a string of digits from 2 to 9, return every letter combination the digit string could spell out under a telephone-keypad mapping, in any order.
Each digit key 2-9 maps to a fixed group of 3 or 4 letters, exactly like on an old telephone keypad; the digit 1 has no letters mapped to it.
2 maps to abc and 3 to def, so there are 3 * 3 = 9 letter pairs.One Choice per Slot
slots in order ➔ one option per slot ➔ record at full length| 1 | from typing import Callable, Iterable, List, TypeVar |
| 2 | |
| 3 | Item = TypeVar("Item") |
| 4 | |
| 5 | |
| 6 | def one_per_slot(slots: int, choices: Callable[[int], Iterable[Item]]) -> List[List[Item]]: |
| 7 | """One Choice per Slot: every way to pick one option for each slot 0..slots-1, in slot order. |
| 8 | choices(slot) lists the options of that slot. No choice limits the options of another slot. |
| 9 | An empty slot gives no full combination, so the result is []. |
| 10 | G = the number of full combinations. T = the cost of one choices call. |
| 11 | Time: O(slots * G * T) | Space: O(slots) path + recursion stack, output not counted |
| 12 | """ |
| 13 | results: List[List[Item]] = [] |
| 14 | path: List[Item] = [] # path[k] is the option chosen for slot k |
| 15 | |
| 16 | def backtrack(index: int) -> None: |
| 17 | # 1. Save a Complete Combination: |
| 18 | # Every slot has an option chosen only once index reaches slots; a shorter |
| 19 | # path still has empty slots, so nothing is saved before that. |
| 20 | if index == slots: |
| 21 | results.append(list(path)) |
| 22 | return |
| 23 | |
| 24 | # 2. Choose, Explore, Un-choose, One Slot's Options Only: |
| 25 | # choices(index) lists only the options of slot index, and no other slot |
| 26 | # limits them, so trying each in turn builds every combination once, one |
| 27 | # slot at a time. |
| 28 | for option in choices(index): |
| 29 | path.append(option) |
| 30 | backtrack(index + 1) |
| 31 | path.pop() |
| 32 | |
| 33 | backtrack(0) |
| 34 | return results |
path holds exactly one option for each of the slots digits[0..index-1], in order, so len(path) == index in every call.Every position of the answer is its own slot with its own list of options, so the recursion depth is the slot: backtrack(index) tries each letter of keypad[digits[index]], appends it to path, and moves on to backtrack(index + 1). The slots never share options and no choice limits another, so there is no start index, no used set and nothing to prune: every branch reaches a full combination, and each combination is built exactly once, one letter per slot in slot order, which is why the answer has exactly the product of the slots' option counts. The trap is where to save: append "".join(path) only when index == len(digits), because a shorter path still has empty slots; saving at every node, as Subsets does, adds partial strings such as "a".
See it run
One Choice per Slot, step by step
index 0path []saved 0
The top row is digits and the bottom row is path. Each column of path is one slot.
choosethe slot that path.append just filledundothe slot that path.pop is about to emptyindexthe digit whose letters backtrack(index) is trying
digits = "23" has 2 slots. path starts empty and backtrack(0) begins.
Gotcha 1 of 4
- The trap
Gotcha 1: Saving
pathbefore every slot has its letterMistakeresults.append("".join(path)) if index == len(digits): returnFixif index == len(digits): results.append("".join(path)) returnWhat happens:
path[k]holds the letter fordigits[k]. Whileindex < len(digits), some slots are still empty. Saving at every node, as Subsets (LC 78) does, also saves those shorter strings, such as""and"a".Example: digits = "23" (the first 4 results)Mistake: returns ['', 'a', 'ad', 'ae']Fix: returns ['ad', 'ae', 'af', 'bd']
- Every choice comes from one shared pool and a chosen item can't be picked again, as in Combinations (LC 77): the positions have no option lists of their own. See Combinations
- Each position has the same few options, but a choice is only legal given the ones before it, as in Generate Parentheses (LC 22): some branches must be cut before they reach full length. See Constraint Backtracking
Deep Dive
Why the depth is the slot
Every position of the answer is a slot. Slot 0 comes from digits[0], slot 1 comes from digits[1], and so on. Each slot has its own list of options. For the digit 2 the options are a, b, c. For the digit 3 they are d, e, f.
backtrack(index) works on slot index only. It loops over keypad[digits[index]]. For each letter it does three things: path.append(letter), then backtrack(index + 1), then path.pop(). The append is the choice, the recursive call fills the later slots, and the pop undoes the choice.
Example: digits = "23". backtrack(0) appends a, so path = ['a']. backtrack(1) appends d, so path = ['a', 'd']. Now index == 2 == len(digits), so "ad" is saved. The pop removes d. The loop then appends e, and "ae" is saved, and so on.
After the three letters of slot 1 are done, backtrack(1) returns. The pop removes a from slot 0, and the loop appends b. The strings come out in this order: ad, ae, af, bd, be, bf, cd, ce, cf. That is 9 strings, which is 3 times 3.
The invariant is len(path) == index in every call. The path always holds one option for each slot before index, and no option for the slots from index onward.
Why each combination is built exactly once
A full combination is one option for each slot. In the recursion, slot 0 picks first, then slot 1, and so on. Two different combinations differ in at least one slot. At the first slot where they differ, the loop took a different letter, so they come from different branches.
No branch is ever cut. No choice in one slot limits the options of another slot. So every branch reaches index == slots, and every full combination appears. The number of saved strings is the product of the option counts of the slots.
I checked this with the card's code on 500 random digit strings. The saved strings were the same as itertools.product gives, in the same order. For "79" the result has 16 strings, because 7 and 9 have 4 options each.
This is the difference from Combinations (LC 77). There, all choices come from one shared pool, and an item cannot be used twice. Here, the slots do not share options, so there is no start index and no used set.
The cost: one path per combination
Let G be the number of full combinations. Each one is built in slots appends. Saving a copy with list(path) or "".join(path) costs slots as well. So the time is O(slots * G * T), where T is the cost of one choices call. The blueprint states this.
For the phone number, a slot has at most 4 options, so G is at most 4^N and the time is O(N * 4^N). The space is O(N) for path and the recursion stack. The saved strings are the output and are not counted.
The number of backtrack calls is smaller than the cost of saving. For 3 slots with 3 options each, I counted 40 calls. That matches (3^4 - 1) / 2 = 40.
The output is the cost. If the answer has G strings, no method can be faster than writing them. So the card has no pruning step to add.
Other problems with one option list per slot
Letter Case Permutation (LC 784): each character of s is a slot. A letter has two options, its lower case and its upper case. A digit has one option, itself. For s = "a1b2" the result is a1b2, a1B2, A1b2, A1B2.
Brace Expansion (LC 1087): each plain letter is a slot with one option, and each brace group is a slot with the listed options. Sort the options of a group first, because the answer must be in sorted order. For {a,b}c{d,e}f the result is acdf, acef, bcdf, bcef.
In both problems only the function choices(index) changes. The saving rule and the choose, recurse, undo loop stay the same.
One edge case: an empty slot gives no full combination, so the result is []. LeetCode's Letter Combinations of a Phone Number (LC 17) also asks for [] when digits is empty. The loop alone would return [""] for that input, so a solution must handle an empty digits before it starts.
60-Second Pitch
"Each position of the answer is a slot with its own list of options, and no choice limits another. So the recursion depth is the slot index. In backtrack(index) I loop over the options of slot index. I append one to path, call backtrack(index + 1), and pop it. I save a copy of path only when index reaches the number of slots, because before that some slots are still empty. Each full combination is built exactly once, so the answer has the product of the option counts. With at most 4 letters per digit, the time is O(N * 4^N), and the space is O(N) for the path and the recursion stack, not counting the output."
Common Questions
- What are the time and space complexity of
letterCombinations? O(N * 4^N) time, because a digit has at most 4 letters and each full string costs N to build and save. The space is
O(N)forpathand the recursion stack. The output is not counted.- Why is there no
startindex orusedset? Each slot has its own options, and no choice limits another slot. A
startindex or ausedset is needed only when the choices come from one shared pool, as in Combinations (LC 77).- Why save only when
index == len(digits)? Before that,
pathhas fewer letters than there are digits, so some slots are empty. Saving at every call, as Subsets (LC 78) does, would add strings such as"a"that are not full combinations.- How would you solve the problem without recursion?
Start with a list that holds the empty string. For each digit, build a new list: for every string in the list and every letter of the digit, add the string plus the letter. This does the same work, and the memory is the size of the list. The recursive version keeps only one path in memory at a time.
- How do you change the code for Letter Case Permutation (LC 784)?
Only the options change. For a letter, use its lower case and upper case. For a digit, use the digit alone. The choose, recurse, undo loop and the base case stay the same.
SWE Reasoning
Spot it
- The answer is a string or list of fixed length, and each position has its own short list of options.
- Any mix of choices is allowed. No choice makes another option illegal.
- The statement says "all possible" and N is small, because the output size is the product of the option counts.
- The options come from the position itself: the digit on a key, the two cases of a letter, the words inside braces.
- If every choice must be legal given the earlier choices, as in Generate Parentheses (LC 22), the signal points to a different card.
The invariant, step by step
backtrack(0)starts with an emptypath.len(path) == 0 == index.- In
backtrack(index), the loop takes one option of slotindexonly, usingchoices(index). path.append(option)makeslen(path) == index + 1. The callbacktrack(index + 1)starts withlen(path) == index + 1, as the invariant requires.- When
backtrack(index + 1)returns,path.pop()makeslen(path) == indexagain. The next option of this slot can start from the same state. - When
index == slots, every slot has an option. Only then does the code save a copy ofpath. - The loop visits each option of each slot, so every full combination is saved once.
Traps
- Saving before every slot is filled. If you save
pathat every call, as in Subsets (LC 78), you also save""and"a". For"23"the first four results are['', 'a', 'ad', 'ae']instead of['ad', 'ae', 'af', 'bd']. - Looping over a shared pool with
range(index, len(digits)). The loop can skip a slot, so strings such as"d"appear. They have no letter for the digit2. - Forgetting
path.pop(). The next letter is appended after the old one. For"23"the first three results are['ad', 'ade', 'adef']instead of['ad', 'ae', 'af']. - Giving a digit two options in Letter Case Permutation (LC 784). For a digit,
ch.lower()andch.upper()are the same character, so every string is built twice. For"a1"the result is['a1', 'a1', 'A1', 'A1']. - Not handling an empty input. For
digits = "", the base caseindex == len(digits)is true at once and the blueprint returns[""]. LeetCode expects[].
Practice
Builds on Subsets, which you haven’t finished yet.
Cartesian Product Backtracking (One Choice per Slot)
start and no used array.Given a string of digits from 2 to 9, return every letter combination the digit string could spell out under a telephone-keypad mapping, in any order.
Each digit key 2-9 maps to a fixed group of 3 or 4 letters, exactly like on an old telephone keypad; the digit 1 has no letters mapped to it.
2 maps to abc and 3 to def, so there are 3 * 3 = 9 letter pairs.One Choice per Slot
slots in order ➔ one option per slot ➔ record at full length| 1 | from typing import Callable, Iterable, List, TypeVar |
| 2 | |
| 3 | Item = TypeVar("Item") |
| 4 | |
| 5 | |
| 6 | def one_per_slot(slots: int, choices: Callable[[int], Iterable[Item]]) -> List[List[Item]]: |
| 7 | """One Choice per Slot: every way to pick one option for each slot 0..slots-1, in slot order. |
| 8 | choices(slot) lists the options of that slot. No choice limits the options of another slot. |
| 9 | An empty slot gives no full combination, so the result is []. |
| 10 | G = the number of full combinations. T = the cost of one choices call. |
| 11 | Time: O(slots * G * T) | Space: O(slots) path + recursion stack, output not counted |
| 12 | """ |
| 13 | results: List[List[Item]] = [] |
| 14 | path: List[Item] = [] # path[k] is the option chosen for slot k |
| 15 | |
| 16 | def backtrack(index: int) -> None: |
| 17 | # 1. Save a Complete Combination: |
| 18 | # Every slot has an option chosen only once index reaches slots; a shorter |
| 19 | # path still has empty slots, so nothing is saved before that. |
| 20 | if index == slots: |
| 21 | results.append(list(path)) |
| 22 | return |
| 23 | |
| 24 | # 2. Choose, Explore, Un-choose, One Slot's Options Only: |
| 25 | # choices(index) lists only the options of slot index, and no other slot |
| 26 | # limits them, so trying each in turn builds every combination once, one |
| 27 | # slot at a time. |
| 28 | for option in choices(index): |
| 29 | path.append(option) |
| 30 | backtrack(index + 1) |
| 31 | path.pop() |
| 32 | |
| 33 | backtrack(0) |
| 34 | return results |
path holds exactly one option for each of the slots digits[0..index-1], in order, so len(path) == index in every call.Every position of the answer is its own slot with its own list of options, so the recursion depth is the slot: backtrack(index) tries each letter of keypad[digits[index]], appends it to path, and moves on to backtrack(index + 1). The slots never share options and no choice limits another, so there is no start index, no used set and nothing to prune: every branch reaches a full combination, and each combination is built exactly once, one letter per slot in slot order, which is why the answer has exactly the product of the slots' option counts. The trap is where to save: append "".join(path) only when index == len(digits), because a shorter path still has empty slots; saving at every node, as Subsets does, adds partial strings such as "a".
See it run
One Choice per Slot, step by step
index 0path []saved 0
The top row is digits and the bottom row is path. Each column of path is one slot.
choosethe slot that path.append just filledundothe slot that path.pop is about to emptyindexthe digit whose letters backtrack(index) is trying
digits = "23" has 2 slots. path starts empty and backtrack(0) begins.
Gotcha 1 of 4
- The trap
Gotcha 1: Saving
pathbefore every slot has its letterMistakeresults.append("".join(path)) if index == len(digits): returnFixif index == len(digits): results.append("".join(path)) returnWhat happens:
path[k]holds the letter fordigits[k]. Whileindex < len(digits), some slots are still empty. Saving at every node, as Subsets (LC 78) does, also saves those shorter strings, such as""and"a".Example: digits = "23" (the first 4 results)Mistake: returns ['', 'a', 'ad', 'ae']Fix: returns ['ad', 'ae', 'af', 'bd']
- Every choice comes from one shared pool and a chosen item can't be picked again, as in Combinations (LC 77): the positions have no option lists of their own. See Combinations
- Each position has the same few options, but a choice is only legal given the ones before it, as in Generate Parentheses (LC 22): some branches must be cut before they reach full length. See Constraint Backtracking
Deep Dive
Why the depth is the slot
Every position of the answer is a slot. Slot 0 comes from digits[0], slot 1 comes from digits[1], and so on. Each slot has its own list of options. For the digit 2 the options are a, b, c. For the digit 3 they are d, e, f.
backtrack(index) works on slot index only. It loops over keypad[digits[index]]. For each letter it does three things: path.append(letter), then backtrack(index + 1), then path.pop(). The append is the choice, the recursive call fills the later slots, and the pop undoes the choice.
Example: digits = "23". backtrack(0) appends a, so path = ['a']. backtrack(1) appends d, so path = ['a', 'd']. Now index == 2 == len(digits), so "ad" is saved. The pop removes d. The loop then appends e, and "ae" is saved, and so on.
After the three letters of slot 1 are done, backtrack(1) returns. The pop removes a from slot 0, and the loop appends b. The strings come out in this order: ad, ae, af, bd, be, bf, cd, ce, cf. That is 9 strings, which is 3 times 3.
The invariant is len(path) == index in every call. The path always holds one option for each slot before index, and no option for the slots from index onward.
Why each combination is built exactly once
A full combination is one option for each slot. In the recursion, slot 0 picks first, then slot 1, and so on. Two different combinations differ in at least one slot. At the first slot where they differ, the loop took a different letter, so they come from different branches.
No branch is ever cut. No choice in one slot limits the options of another slot. So every branch reaches index == slots, and every full combination appears. The number of saved strings is the product of the option counts of the slots.
I checked this with the card's code on 500 random digit strings. The saved strings were the same as itertools.product gives, in the same order. For "79" the result has 16 strings, because 7 and 9 have 4 options each.
This is the difference from Combinations (LC 77). There, all choices come from one shared pool, and an item cannot be used twice. Here, the slots do not share options, so there is no start index and no used set.
The cost: one path per combination
Let G be the number of full combinations. Each one is built in slots appends. Saving a copy with list(path) or "".join(path) costs slots as well. So the time is O(slots * G * T), where T is the cost of one choices call. The blueprint states this.
For the phone number, a slot has at most 4 options, so G is at most 4^N and the time is O(N * 4^N). The space is O(N) for path and the recursion stack. The saved strings are the output and are not counted.
The number of backtrack calls is smaller than the cost of saving. For 3 slots with 3 options each, I counted 40 calls. That matches (3^4 - 1) / 2 = 40.
The output is the cost. If the answer has G strings, no method can be faster than writing them. So the card has no pruning step to add.
Other problems with one option list per slot
Letter Case Permutation (LC 784): each character of s is a slot. A letter has two options, its lower case and its upper case. A digit has one option, itself. For s = "a1b2" the result is a1b2, a1B2, A1b2, A1B2.
Brace Expansion (LC 1087): each plain letter is a slot with one option, and each brace group is a slot with the listed options. Sort the options of a group first, because the answer must be in sorted order. For {a,b}c{d,e}f the result is acdf, acef, bcdf, bcef.
In both problems only the function choices(index) changes. The saving rule and the choose, recurse, undo loop stay the same.
One edge case: an empty slot gives no full combination, so the result is []. LeetCode's Letter Combinations of a Phone Number (LC 17) also asks for [] when digits is empty. The loop alone would return [""] for that input, so a solution must handle an empty digits before it starts.
60-Second Pitch
"Each position of the answer is a slot with its own list of options, and no choice limits another. So the recursion depth is the slot index. In backtrack(index) I loop over the options of slot index. I append one to path, call backtrack(index + 1), and pop it. I save a copy of path only when index reaches the number of slots, because before that some slots are still empty. Each full combination is built exactly once, so the answer has the product of the option counts. With at most 4 letters per digit, the time is O(N * 4^N), and the space is O(N) for the path and the recursion stack, not counting the output."
Common Questions
- What are the time and space complexity of
letterCombinations? O(N * 4^N) time, because a digit has at most 4 letters and each full string costs N to build and save. The space is
O(N)forpathand the recursion stack. The output is not counted.- Why is there no
startindex orusedset? Each slot has its own options, and no choice limits another slot. A
startindex or ausedset is needed only when the choices come from one shared pool, as in Combinations (LC 77).- Why save only when
index == len(digits)? Before that,
pathhas fewer letters than there are digits, so some slots are empty. Saving at every call, as Subsets (LC 78) does, would add strings such as"a"that are not full combinations.- How would you solve the problem without recursion?
Start with a list that holds the empty string. For each digit, build a new list: for every string in the list and every letter of the digit, add the string plus the letter. This does the same work, and the memory is the size of the list. The recursive version keeps only one path in memory at a time.
- How do you change the code for Letter Case Permutation (LC 784)?
Only the options change. For a letter, use its lower case and upper case. For a digit, use the digit alone. The choose, recurse, undo loop and the base case stay the same.
SWE Reasoning
Spot it
- The answer is a string or list of fixed length, and each position has its own short list of options.
- Any mix of choices is allowed. No choice makes another option illegal.
- The statement says "all possible" and N is small, because the output size is the product of the option counts.
- The options come from the position itself: the digit on a key, the two cases of a letter, the words inside braces.
- If every choice must be legal given the earlier choices, as in Generate Parentheses (LC 22), the signal points to a different card.
The invariant, step by step
backtrack(0)starts with an emptypath.len(path) == 0 == index.- In
backtrack(index), the loop takes one option of slotindexonly, usingchoices(index). path.append(option)makeslen(path) == index + 1. The callbacktrack(index + 1)starts withlen(path) == index + 1, as the invariant requires.- When
backtrack(index + 1)returns,path.pop()makeslen(path) == indexagain. The next option of this slot can start from the same state. - When
index == slots, every slot has an option. Only then does the code save a copy ofpath. - The loop visits each option of each slot, so every full combination is saved once.
Traps
- Saving before every slot is filled. If you save
pathat every call, as in Subsets (LC 78), you also save""and"a". For"23"the first four results are['', 'a', 'ad', 'ae']instead of['ad', 'ae', 'af', 'bd']. - Looping over a shared pool with
range(index, len(digits)). The loop can skip a slot, so strings such as"d"appear. They have no letter for the digit2. - Forgetting
path.pop(). The next letter is appended after the old one. For"23"the first three results are['ad', 'ade', 'adef']instead of['ad', 'ae', 'af']. - Giving a digit two options in Letter Case Permutation (LC 784). For a digit,
ch.lower()andch.upper()are the same character, so every string is built twice. For"a1"the result is['a1', 'a1', 'A1', 'A1']. - Not handling an empty input. For
digits = "", the base caseindex == len(digits)is true at once and the blueprint returns[""]. LeetCode expects[].