Builds on Subsets, which you haven’t finished yet.
used array replaces start, so any element not yet in path can come next, and each element is used once.Permutations (LC 46)(opens leetcode.com)
Given an array nums of distinct integers, return every possible ordering of all its elements -- i.e., every permutation -- in any order.
A permutation uses each element from nums exactly once, arranged into some linear sequence.
3 * 2 * 1 = 6 ways.Permutations
elements ➔ unused candidate selection ➔ boolean visited array| 1 | from typing import Callable, List, TypeVar |
| 2 | |
| 3 | Item = TypeVar("Item") |
| 4 | |
| 5 | |
| 6 | def permutations(items: List[Item], can_place: Callable[[List[Item], Item], bool]) -> List[List[Item]]: |
| 7 | """Permutations: every ordering of items, each position filled with an item that is not in path yet. |
| 8 | can_place(path, item) says whether item may come next after path. It prunes a branch early. |
| 9 | Precondition: equal items count as different items, so equal values give repeated orderings. |
| 10 | For equal values (Permutations II), sort the values and pass (value, index) pairs. |
| 11 | Then skip a pair when the equal pair just before it is not in path yet. |
| 12 | N = len(items). T = the cost of one can_place call. |
| 13 | Time: O(N * N! * T) | Space: O(N) path + recursion stack, output not counted |
| 14 | """ |
| 15 | results: List[List[Item]] = [] |
| 16 | path: List[Item] = [] |
| 17 | used = [False] * len(items) # used[i] is true exactly when items[i] is in path |
| 18 | |
| 19 | def backtrack() -> None: |
| 20 | # 1. Save a Full Ordering: |
| 21 | # path only counts as one ordering once it holds every item, so stop and save |
| 22 | # it there; a shorter path is still missing items and is not a real answer. |
| 23 | if len(path) == len(items): |
| 24 | results.append(list(path)) |
| 25 | return |
| 26 | |
| 27 | for i in range(len(items)): |
| 28 | # 2. Offer Every Unused Item: |
| 29 | # The loop restarts at 0 every call, so any item not already in path may |
| 30 | # come next; used[i] and can_place rule out the ones that cannot. |
| 31 | if used[i] or not can_place(path, items[i]): |
| 32 | continue |
| 33 | |
| 34 | # 3. Choose, Explore, Un-choose: |
| 35 | # Mark items[i] used and append it, recurse so the next position sees it |
| 36 | # taken, then undo both marks so a later i in this loop can still use it. |
| 37 | used[i] = True |
| 38 | path.append(items[i]) |
| 39 | backtrack() |
| 40 | path.pop() |
| 41 | used[i] = False |
| 42 | |
| 43 | backtrack() |
| 44 | return results |
used[i] is true exactly when nums[i] is in path, so path never holds the same element twice.Every position may hold any element that is not already in path, so the loop always starts at 0 and skips i when used[i] is true. Mark used[i] and append before backtrack(), then pop and unmark after it: each full path is then a different sequence of choices, and with distinct values a different ordering, so every ordering is saved exactly once when len(path) == len(nums). The trap is the undo: forget used[i] = False after the call and every later branch treats that element as taken, so most orderings never appear.
See it run
Permutations, step by step
path []saved 0
The top row is nums. The middle row is used. The bottom row is path, filled from the left.
ithe index the loop is looking atused[i]the used flag that is read or set to Truepaththe slot just added to pathundothe slot or flag just reset by the undo
nums is [1, 2]. path is empty, and every used[i] is False.
Gotcha 1 of 4
- The trap
Gotcha 1: Leaving out
used[i] = Falseafter the recursive callMistakepath.pop()Fixpath.pop() used[i] = FalseWhat happens:
used[i]becomes True whennums[i]is placed. Without the reset, it stays True after the branch returns. Every later branch then skipsnums[i]. So only the first ordering is saved.Example: nums = [1, 2]Mistake: returns [[1, 2]]Fix: returns [[1, 2], [2, 1]]
- Order does not matter, as in Combinations (LC 77):
[1, 2]and[2, 1]are the same answer, so listing every ordering returns duplicates. See Combinations
Deep Dive
What changes from Subsets: the loop restarts at 0
The Subsets card builds each group in index order, so its loop starts at start. A permutation is an ordering, so [1, 2] and [2, 1] are two different answers. Every position may hold any item that is not in path yet.
So the loop starts at 0 in every call, and a used array records which items are already in path. The loop skips i when used[i] is True.
A permutation must use every item. For this reason the code saves only when len(path) == len(nums). Saving at every node, as Subsets does, would also save the shorter lists.
Why each ordering appears exactly once
At the first position, the loop offers all N items. At the second position, it offers the N - 1 items that are not used. At the third position, it offers N - 2, and so on. The number of full paths is N * (N - 1) * ... * 1, which is N!.
Two different full paths differ at the first position where the loop picked a different index. Each full path ends the walk at its own leaf, so no ordering is saved twice. Each ordering of different values is reachable by one sequence of picks, so none is missed.
For nums = [1, 2, 3], the result has 6 orderings, and backtrack runs 16 times: 1 first call, 3 at depth 1, 6 at depth 2 and 6 at depth 3.
Why the undo has two lines
Take nums = [1, 2]. Choosing does two things: used[i] = True and path.append(nums[i]). So undoing must also do two things: path.pop() and used[i] = False.
The board shows both lines as two steps. The first pop returns path to [1], but used[1] is still True. Only the second line makes nums[1] available again.
Without used[i] = False, permute([1, 2]) returns [[1, 2]], not [[1, 2], [2, 1]]. After the first branch, used[0] and used[1] both stay True, so the second branch has nothing to place.
The cost, and repeated values
There are N! leaves, and saving each one copies path in O(N). The time is O(N * N!). The space is O(N) for path, used and the recursion stack. The output is not counted.
For N = 10, there are 3,628,800 orderings. For N = 12, there are 479,001,600, which is too many to list. Check the limit on N before you choose this search.
The card's precondition says equal values count as different items. For [1, 1, 2], the search returns 6 orderings, but only 3 are different. Permutations II (LC 47) removes the repeats: sort nums, then skip i when i > 0, nums[i] == nums[i - 1] and used[i - 1] is False. That makes equal values enter path from left to right, so each different ordering is saved once.
60-Second Pitch
"Order matters and every item is used once, so each position may hold any item that is not in path. The loop starts at 0 in every call, and used[i] marks the items already placed. I choose by setting used[i] = True and appending to path. I recurse, then I undo both: path.pop() and used[i] = False. I save a copy of path when its length equals len(nums). There are N! orderings and each copy costs O(N), so the time is O(N * N!). The space is O(N). I would prune with a rule check when I place an item, if the problem has such a rule."
Common Questions
- How is this different from Subsets (LC 78)?
Subsets builds each group in index order, so its loop starts at
startand it saves at every node. Permutations restarts the loop at 0 in every call, usesusedto avoid repeating an item, and saves only whenpathhas all the items.- What are the time and space complexity?
O(N * N!) time, because there are N! leaves and each copy costs
O(N). The space isO(N)forpath,usedand the recursion stack. The output is not counted.- What happens if the input has equal values?
Equal values count as different items, so the same ordering is saved several times. For
[1, 1, 2], the search returns 6 orderings and only 3 are different. Permutations II (LC 47) sortsnumsand skipsiwheni > 0,nums[i] == nums[i - 1]andused[i - 1]is False.- Could you swap items in place instead of using
used? Yes. You can swap
nums[start]withnums[i], recurse onstart + 1, and swap back. It uses nousedarray. The undo is still required, and with equal values the repeat check is harder, so theusedversion is easier to get right in an interview.- How do you cut the search when the problem has a rule?
Test the rule when you place an item, before
backtrack()runs. In Beautiful Arrangement (LC 526), a number that breaks the rule at its position is not placed, so the whole branch below it is never built.
SWE Reasoning
Spot it
- The statement asks for all arrangements, orderings or assignments, and the order of the answer matters.
- Every item is used exactly once, so a full answer has length
len(nums). - A rule can reject a placement early, for example a number must or must not stand at a given position, as in Beautiful Arrangement (LC 526).
- The input is small, usually 8 to 10 items, because the answer has N! orderings.
- You can say: for this position, which items are still free?
The invariant, step by step
backtrack()starts. Iflen(path) == len(nums), it saveslist(path)and returns.- Otherwise the loop runs
ifrom 0 to the end. Ifused[i]is True, thennums[i]is already inpath, socontinueskips it. - For a free
i, the code setsused[i] = Trueand runspath.append(nums[i]). The invariant holds:used[i]is True exactly whennums[i]is inpath. backtrack()fills the remaining positions with the items that are still free.path.pop()andused[i] = Falserestore both.pathandusedare now as they were before step 3, so the nextisees a correct state.- The picks form a choice for each position, so each full
pathis a different ordering and is saved once.
Traps
- No
used[i] = Falseafter the call. The flag stays True after its branch returns, so every later branch skips that item.permute([1, 2])returns[[1, 2]]. - Saving at every node. A permutation needs all items. Saving at every call also saves
[]and every shorter list.permute([1, 2])returns[[], [1], [1, 2], [2], [2, 1]]. - Saving
pathand notlist(path). The code saves one list object and keeps changing it, so the saved lists are all empty at the end. - Checking the rule only on a full ordering. In Beautiful Arrangement (LC 526), the rule belongs to each position. Test it when you place the number, so a bad number cuts its whole branch. Testing at the end still builds all n! orderings.
- A large
N.N = 12needs about 479 million orderings. Read the constraints first.
Practice
Builds on Subsets, which you haven’t finished yet.
used array replaces start, so any element not yet in path can come next, and each element is used once.Permutations (LC 46)(opens leetcode.com)
Given an array nums of distinct integers, return every possible ordering of all its elements -- i.e., every permutation -- in any order.
A permutation uses each element from nums exactly once, arranged into some linear sequence.
3 * 2 * 1 = 6 ways.Permutations
elements ➔ unused candidate selection ➔ boolean visited array| 1 | from typing import Callable, List, TypeVar |
| 2 | |
| 3 | Item = TypeVar("Item") |
| 4 | |
| 5 | |
| 6 | def permutations(items: List[Item], can_place: Callable[[List[Item], Item], bool]) -> List[List[Item]]: |
| 7 | """Permutations: every ordering of items, each position filled with an item that is not in path yet. |
| 8 | can_place(path, item) says whether item may come next after path. It prunes a branch early. |
| 9 | Precondition: equal items count as different items, so equal values give repeated orderings. |
| 10 | For equal values (Permutations II), sort the values and pass (value, index) pairs. |
| 11 | Then skip a pair when the equal pair just before it is not in path yet. |
| 12 | N = len(items). T = the cost of one can_place call. |
| 13 | Time: O(N * N! * T) | Space: O(N) path + recursion stack, output not counted |
| 14 | """ |
| 15 | results: List[List[Item]] = [] |
| 16 | path: List[Item] = [] |
| 17 | used = [False] * len(items) # used[i] is true exactly when items[i] is in path |
| 18 | |
| 19 | def backtrack() -> None: |
| 20 | # 1. Save a Full Ordering: |
| 21 | # path only counts as one ordering once it holds every item, so stop and save |
| 22 | # it there; a shorter path is still missing items and is not a real answer. |
| 23 | if len(path) == len(items): |
| 24 | results.append(list(path)) |
| 25 | return |
| 26 | |
| 27 | for i in range(len(items)): |
| 28 | # 2. Offer Every Unused Item: |
| 29 | # The loop restarts at 0 every call, so any item not already in path may |
| 30 | # come next; used[i] and can_place rule out the ones that cannot. |
| 31 | if used[i] or not can_place(path, items[i]): |
| 32 | continue |
| 33 | |
| 34 | # 3. Choose, Explore, Un-choose: |
| 35 | # Mark items[i] used and append it, recurse so the next position sees it |
| 36 | # taken, then undo both marks so a later i in this loop can still use it. |
| 37 | used[i] = True |
| 38 | path.append(items[i]) |
| 39 | backtrack() |
| 40 | path.pop() |
| 41 | used[i] = False |
| 42 | |
| 43 | backtrack() |
| 44 | return results |
used[i] is true exactly when nums[i] is in path, so path never holds the same element twice.Every position may hold any element that is not already in path, so the loop always starts at 0 and skips i when used[i] is true. Mark used[i] and append before backtrack(), then pop and unmark after it: each full path is then a different sequence of choices, and with distinct values a different ordering, so every ordering is saved exactly once when len(path) == len(nums). The trap is the undo: forget used[i] = False after the call and every later branch treats that element as taken, so most orderings never appear.
See it run
Permutations, step by step
path []saved 0
The top row is nums. The middle row is used. The bottom row is path, filled from the left.
ithe index the loop is looking atused[i]the used flag that is read or set to Truepaththe slot just added to pathundothe slot or flag just reset by the undo
nums is [1, 2]. path is empty, and every used[i] is False.
Gotcha 1 of 4
- The trap
Gotcha 1: Leaving out
used[i] = Falseafter the recursive callMistakepath.pop()Fixpath.pop() used[i] = FalseWhat happens:
used[i]becomes True whennums[i]is placed. Without the reset, it stays True after the branch returns. Every later branch then skipsnums[i]. So only the first ordering is saved.Example: nums = [1, 2]Mistake: returns [[1, 2]]Fix: returns [[1, 2], [2, 1]]
- Order does not matter, as in Combinations (LC 77):
[1, 2]and[2, 1]are the same answer, so listing every ordering returns duplicates. See Combinations
Deep Dive
What changes from Subsets: the loop restarts at 0
The Subsets card builds each group in index order, so its loop starts at start. A permutation is an ordering, so [1, 2] and [2, 1] are two different answers. Every position may hold any item that is not in path yet.
So the loop starts at 0 in every call, and a used array records which items are already in path. The loop skips i when used[i] is True.
A permutation must use every item. For this reason the code saves only when len(path) == len(nums). Saving at every node, as Subsets does, would also save the shorter lists.
Why each ordering appears exactly once
At the first position, the loop offers all N items. At the second position, it offers the N - 1 items that are not used. At the third position, it offers N - 2, and so on. The number of full paths is N * (N - 1) * ... * 1, which is N!.
Two different full paths differ at the first position where the loop picked a different index. Each full path ends the walk at its own leaf, so no ordering is saved twice. Each ordering of different values is reachable by one sequence of picks, so none is missed.
For nums = [1, 2, 3], the result has 6 orderings, and backtrack runs 16 times: 1 first call, 3 at depth 1, 6 at depth 2 and 6 at depth 3.
Why the undo has two lines
Take nums = [1, 2]. Choosing does two things: used[i] = True and path.append(nums[i]). So undoing must also do two things: path.pop() and used[i] = False.
The board shows both lines as two steps. The first pop returns path to [1], but used[1] is still True. Only the second line makes nums[1] available again.
Without used[i] = False, permute([1, 2]) returns [[1, 2]], not [[1, 2], [2, 1]]. After the first branch, used[0] and used[1] both stay True, so the second branch has nothing to place.
The cost, and repeated values
There are N! leaves, and saving each one copies path in O(N). The time is O(N * N!). The space is O(N) for path, used and the recursion stack. The output is not counted.
For N = 10, there are 3,628,800 orderings. For N = 12, there are 479,001,600, which is too many to list. Check the limit on N before you choose this search.
The card's precondition says equal values count as different items. For [1, 1, 2], the search returns 6 orderings, but only 3 are different. Permutations II (LC 47) removes the repeats: sort nums, then skip i when i > 0, nums[i] == nums[i - 1] and used[i - 1] is False. That makes equal values enter path from left to right, so each different ordering is saved once.
60-Second Pitch
"Order matters and every item is used once, so each position may hold any item that is not in path. The loop starts at 0 in every call, and used[i] marks the items already placed. I choose by setting used[i] = True and appending to path. I recurse, then I undo both: path.pop() and used[i] = False. I save a copy of path when its length equals len(nums). There are N! orderings and each copy costs O(N), so the time is O(N * N!). The space is O(N). I would prune with a rule check when I place an item, if the problem has such a rule."
Common Questions
- How is this different from Subsets (LC 78)?
Subsets builds each group in index order, so its loop starts at
startand it saves at every node. Permutations restarts the loop at 0 in every call, usesusedto avoid repeating an item, and saves only whenpathhas all the items.- What are the time and space complexity?
O(N * N!) time, because there are N! leaves and each copy costs
O(N). The space isO(N)forpath,usedand the recursion stack. The output is not counted.- What happens if the input has equal values?
Equal values count as different items, so the same ordering is saved several times. For
[1, 1, 2], the search returns 6 orderings and only 3 are different. Permutations II (LC 47) sortsnumsand skipsiwheni > 0,nums[i] == nums[i - 1]andused[i - 1]is False.- Could you swap items in place instead of using
used? Yes. You can swap
nums[start]withnums[i], recurse onstart + 1, and swap back. It uses nousedarray. The undo is still required, and with equal values the repeat check is harder, so theusedversion is easier to get right in an interview.- How do you cut the search when the problem has a rule?
Test the rule when you place an item, before
backtrack()runs. In Beautiful Arrangement (LC 526), a number that breaks the rule at its position is not placed, so the whole branch below it is never built.
SWE Reasoning
Spot it
- The statement asks for all arrangements, orderings or assignments, and the order of the answer matters.
- Every item is used exactly once, so a full answer has length
len(nums). - A rule can reject a placement early, for example a number must or must not stand at a given position, as in Beautiful Arrangement (LC 526).
- The input is small, usually 8 to 10 items, because the answer has N! orderings.
- You can say: for this position, which items are still free?
The invariant, step by step
backtrack()starts. Iflen(path) == len(nums), it saveslist(path)and returns.- Otherwise the loop runs
ifrom 0 to the end. Ifused[i]is True, thennums[i]is already inpath, socontinueskips it. - For a free
i, the code setsused[i] = Trueand runspath.append(nums[i]). The invariant holds:used[i]is True exactly whennums[i]is inpath. backtrack()fills the remaining positions with the items that are still free.path.pop()andused[i] = Falserestore both.pathandusedare now as they were before step 3, so the nextisees a correct state.- The picks form a choice for each position, so each full
pathis a different ordering and is saved once.
Traps
- No
used[i] = Falseafter the call. The flag stays True after its branch returns, so every later branch skips that item.permute([1, 2])returns[[1, 2]]. - Saving at every node. A permutation needs all items. Saving at every call also saves
[]and every shorter list.permute([1, 2])returns[[], [1], [1, 2], [2], [2, 1]]. - Saving
pathand notlist(path). The code saves one list object and keeps changing it, so the saved lists are all empty at the end. - Checking the rule only on a full ordering. In Beautiful Arrangement (LC 526), the rule belongs to each position. Test it when you place the number, so a bad number cuts its whole branch. Testing at the end still builds all n! orderings.
- A large
N.N = 12needs about 479 million orderings. Read the constraints first.