You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
hello-algo/en/docs/chapter_backtracking/permutations_problem.md

434 lines
15 KiB

7 months ago
---
comments: true
---
# 13.2   Permutation problem
The permutation problem is a typical application of the backtracking algorithm. It is defined as finding all possible arrangements of elements from a given set (such as an array or string).
7 months ago
Table 13-2 lists several example data, including the input arrays and their corresponding permutations.
7 months ago
<p align="center"> Table 13-2 &nbsp; Permutation examples </p>
<div class="center-table" markdown>
7 months ago
| Input array | Permutations |
7 months ago
| :---------- | :----------------------------------------------------------------- |
| $[1]$ | $[1]$ |
| $[1, 2]$ | $[1, 2], [2, 1]$ |
| $[1, 2, 3]$ | $[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]$ |
</div>
## 13.2.1 &nbsp; Cases without equal elements
!!! question
Enter an integer array without duplicate elements and return all possible permutations.
From the perspective of the backtracking algorithm, **we can imagine the process of generating permutations as a series of choices**. Suppose the input array is $[1, 2, 3]$, if we first choose $1$, then $3$, and finally $2$, we obtain the permutation $[1, 3, 2]$. Backtracking means undoing a choice and then continuing to try other choices.
From the code perspective, the candidate set `choices` contains all elements of the input array, and the state `state` contains elements that have been selected so far. Please note that each element can only be chosen once, **thus all elements in `state` must be unique**.
7 months ago
As shown in Figure 13-5, we can unfold the search process into a recursive tree, where each node represents the current state `state`. Starting from the root node, after three rounds of choices, we reach the leaf nodes, each corresponding to a permutation.
7 months ago
![Permutation recursive tree](permutations_problem.assets/permutations_i.png){ class="animation-figure" }
<p align="center"> Figure 13-5 &nbsp; Permutation recursive tree </p>
### 1. &nbsp; Pruning of repeated choices
To ensure that each element is selected only once, we consider introducing a boolean array `selected`, where `selected[i]` indicates whether `choices[i]` has been selected. We base our pruning operations on this array:
- After making the choice `choice[i]`, we set `selected[i]` to $\text{True}$, indicating it has been chosen.
- When iterating through the choice list `choices`, skip all nodes that have already been selected, i.e., prune.
7 months ago
As shown in Figure 13-6, suppose we choose 1 in the first round, 3 in the second round, and 2 in the third round, we need to prune the branch of element 1 in the second round and elements 1 and 3 in the third round.
7 months ago
![Permutation pruning example](permutations_problem.assets/permutations_i_pruning.png){ class="animation-figure" }
<p align="center"> Figure 13-6 &nbsp; Permutation pruning example </p>
7 months ago
Observing Figure 13-6, this pruning operation reduces the search space size from $O(n^n)$ to $O(n!)$.
7 months ago
### 2. &nbsp; Code implementation
After understanding the above information, we can "fill in the blanks" in the framework code. To shorten the overall code, we do not implement individual functions within the framework code separately, but expand them in the `backtrack()` function:
=== "Python"
```python title="permutations_i.py"
def backtrack(
state: list[int], choices: list[int], selected: list[bool], res: list[list[int]]
):
7 months ago
"""Backtracking algorithm: Permutation I"""
# When the state length equals the number of elements, record the solution
7 months ago
if len(state) == len(choices):
res.append(list(state))
return
7 months ago
# Traverse all choices
7 months ago
for i, choice in enumerate(choices):
7 months ago
# Pruning: do not allow repeated selection of elements
7 months ago
if not selected[i]:
7 months ago
# Attempt: make a choice, update the state
7 months ago
selected[i] = True
state.append(choice)
7 months ago
# Proceed to the next round of selection
7 months ago
backtrack(state, choices, selected, res)
7 months ago
# Retract: undo the choice, restore to the previous state
7 months ago
selected[i] = False
state.pop()
def permutations_i(nums: list[int]) -> list[list[int]]:
7 months ago
"""Permutation I"""
7 months ago
res = []
backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res)
return res
```
=== "C++"
```cpp title="permutations_i.cpp"
7 months ago
[class]{}-[func]{backtrack}
7 months ago
7 months ago
[class]{}-[func]{permutationsI}
7 months ago
```
=== "Java"
```java title="permutations_i.java"
7 months ago
/* Backtracking algorithm: Permutation I */
7 months ago
void backtrack(List<Integer> state, int[] choices, boolean[] selected, List<List<Integer>> res) {
7 months ago
// When the state length equals the number of elements, record the solution
7 months ago
if (state.size() == choices.length) {
res.add(new ArrayList<Integer>(state));
return;
}
7 months ago
// Traverse all choices
7 months ago
for (int i = 0; i < choices.length; i++) {
int choice = choices[i];
7 months ago
// Pruning: do not allow repeated selection of elements
7 months ago
if (!selected[i]) {
7 months ago
// Attempt: make a choice, update the state
7 months ago
selected[i] = true;
state.add(choice);
7 months ago
// Proceed to the next round of selection
7 months ago
backtrack(state, choices, selected, res);
7 months ago
// Retract: undo the choice, restore to the previous state
7 months ago
selected[i] = false;
state.remove(state.size() - 1);
}
}
}
7 months ago
/* Permutation I */
7 months ago
List<List<Integer>> permutationsI(int[] nums) {
List<List<Integer>> res = new ArrayList<List<Integer>>();
backtrack(new ArrayList<Integer>(), nums, new boolean[nums.length], res);
return res;
}
```
=== "C#"
```csharp title="permutations_i.cs"
7 months ago
[class]{permutations_i}-[func]{Backtrack}
7 months ago
7 months ago
[class]{permutations_i}-[func]{PermutationsI}
7 months ago
```
=== "Go"
```go title="permutations_i.go"
7 months ago
[class]{}-[func]{backtrackI}
7 months ago
7 months ago
[class]{}-[func]{permutationsI}
7 months ago
```
=== "Swift"
```swift title="permutations_i.swift"
7 months ago
[class]{}-[func]{backtrack}
7 months ago
7 months ago
[class]{}-[func]{permutationsI}
7 months ago
```
=== "JS"
```javascript title="permutations_i.js"
7 months ago
[class]{}-[func]{backtrack}
7 months ago
7 months ago
[class]{}-[func]{permutationsI}
7 months ago
```
=== "TS"
```typescript title="permutations_i.ts"
7 months ago
[class]{}-[func]{backtrack}
7 months ago
7 months ago
[class]{}-[func]{permutationsI}
7 months ago
```
=== "Dart"
```dart title="permutations_i.dart"
7 months ago
[class]{}-[func]{backtrack}
7 months ago
7 months ago
[class]{}-[func]{permutationsI}
7 months ago
```
=== "Rust"
```rust title="permutations_i.rs"
7 months ago
[class]{}-[func]{backtrack}
7 months ago
7 months ago
[class]{}-[func]{permutations_i}
7 months ago
```
=== "C"
```c title="permutations_i.c"
7 months ago
[class]{}-[func]{backtrack}
7 months ago
7 months ago
[class]{}-[func]{permutationsI}
7 months ago
```
=== "Kotlin"
```kotlin title="permutations_i.kt"
7 months ago
[class]{}-[func]{backtrack}
7 months ago
7 months ago
[class]{}-[func]{permutationsI}
7 months ago
```
=== "Ruby"
```ruby title="permutations_i.rb"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutations_i}
```
=== "Zig"
```zig title="permutations_i.zig"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsI}
```
## 13.2.2 &nbsp; Considering cases with equal elements
!!! question
Enter an integer array, **which may contain duplicate elements**, and return all unique permutations.
Suppose the input array is $[1, 1, 2]$. To differentiate the two duplicate elements $1$, we mark the second $1$ as $\hat{1}$.
7 months ago
As shown in Figure 13-7, half of the permutations generated by the above method are duplicates.
7 months ago
![Duplicate permutations](permutations_problem.assets/permutations_ii.png){ class="animation-figure" }
<p align="center"> Figure 13-7 &nbsp; Duplicate permutations </p>
So, how do we eliminate duplicate permutations? Most directly, consider using a hash set to deduplicate permutation results. However, this is not elegant, **as branches generating duplicate permutations are unnecessary and should be identified and pruned in advance**, which can further improve algorithm efficiency.
### 1. &nbsp; Pruning of equal elements
7 months ago
Observing Figure 13-8, in the first round, choosing $1$ or $\hat{1}$ results in identical permutations under both choices, thus we should prune $\hat{1}$.
7 months ago
Similarly, after choosing $2$ in the first round, choosing $1$ and $\hat{1}$ in the second round also produces duplicate branches, so we should also prune $\hat{1}$ in the second round.
Essentially, **our goal is to ensure that multiple equal elements are only selected once in each round of choices**.
![Duplicate permutations pruning](permutations_problem.assets/permutations_ii_pruning.png){ class="animation-figure" }
<p align="center"> Figure 13-8 &nbsp; Duplicate permutations pruning </p>
### 2. &nbsp; Code implementation
Based on the code from the previous problem, we consider initiating a hash set `duplicated` in each round of choices, used to record elements that have been tried in that round, and prune duplicate elements:
=== "Python"
```python title="permutations_ii.py"
def backtrack(
state: list[int], choices: list[int], selected: list[bool], res: list[list[int]]
):
7 months ago
"""Backtracking algorithm: Permutation II"""
# When the state length equals the number of elements, record the solution
7 months ago
if len(state) == len(choices):
res.append(list(state))
return
7 months ago
# Traverse all choices
7 months ago
duplicated = set[int]()
for i, choice in enumerate(choices):
7 months ago
# Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements
7 months ago
if not selected[i] and choice not in duplicated:
7 months ago
# Attempt: make a choice, update the state
duplicated.add(choice) # Record selected element values
7 months ago
selected[i] = True
state.append(choice)
7 months ago
# Proceed to the next round of selection
7 months ago
backtrack(state, choices, selected, res)
7 months ago
# Retract: undo the choice, restore to the previous state
7 months ago
selected[i] = False
state.pop()
def permutations_ii(nums: list[int]) -> list[list[int]]:
7 months ago
"""Permutation II"""
7 months ago
res = []
backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res)
return res
```
=== "C++"
```cpp title="permutations_ii.cpp"
7 months ago
[class]{}-[func]{backtrack}
7 months ago
7 months ago
[class]{}-[func]{permutationsII}
7 months ago
```
=== "Java"
```java title="permutations_ii.java"
7 months ago
/* Backtracking algorithm: Permutation II */
7 months ago
void backtrack(List<Integer> state, int[] choices, boolean[] selected, List<List<Integer>> res) {
7 months ago
// When the state length equals the number of elements, record the solution
7 months ago
if (state.size() == choices.length) {
res.add(new ArrayList<Integer>(state));
return;
}
7 months ago
// Traverse all choices
7 months ago
Set<Integer> duplicated = new HashSet<Integer>();
for (int i = 0; i < choices.length; i++) {
int choice = choices[i];
7 months ago
// Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements
7 months ago
if (!selected[i] && !duplicated.contains(choice)) {
7 months ago
// Attempt: make a choice, update the state
duplicated.add(choice); // Record selected element values
7 months ago
selected[i] = true;
state.add(choice);
7 months ago
// Proceed to the next round of selection
7 months ago
backtrack(state, choices, selected, res);
7 months ago
// Retract: undo the choice, restore to the previous state
7 months ago
selected[i] = false;
state.remove(state.size() - 1);
}
}
}
7 months ago
/* Permutation II */
7 months ago
List<List<Integer>> permutationsII(int[] nums) {
List<List<Integer>> res = new ArrayList<List<Integer>>();
backtrack(new ArrayList<Integer>(), nums, new boolean[nums.length], res);
return res;
}
```
=== "C#"
```csharp title="permutations_ii.cs"
7 months ago
[class]{permutations_ii}-[func]{Backtrack}
7 months ago
7 months ago
[class]{permutations_ii}-[func]{PermutationsII}
7 months ago
```
=== "Go"
```go title="permutations_ii.go"
7 months ago
[class]{}-[func]{backtrackII}
7 months ago
7 months ago
[class]{}-[func]{permutationsII}
7 months ago
```
=== "Swift"
```swift title="permutations_ii.swift"
7 months ago
[class]{}-[func]{backtrack}
7 months ago
7 months ago
[class]{}-[func]{permutationsII}
7 months ago
```
=== "JS"
```javascript title="permutations_ii.js"
7 months ago
[class]{}-[func]{backtrack}
7 months ago
7 months ago
[class]{}-[func]{permutationsII}
7 months ago
```
=== "TS"
```typescript title="permutations_ii.ts"
7 months ago
[class]{}-[func]{backtrack}
7 months ago
7 months ago
[class]{}-[func]{permutationsII}
7 months ago
```
=== "Dart"
```dart title="permutations_ii.dart"
7 months ago
[class]{}-[func]{backtrack}
7 months ago
7 months ago
[class]{}-[func]{permutationsII}
7 months ago
```
=== "Rust"
```rust title="permutations_ii.rs"
7 months ago
[class]{}-[func]{backtrack}
7 months ago
7 months ago
[class]{}-[func]{permutations_ii}
7 months ago
```
=== "C"
```c title="permutations_ii.c"
7 months ago
[class]{}-[func]{backtrack}
7 months ago
7 months ago
[class]{}-[func]{permutationsII}
7 months ago
```
=== "Kotlin"
```kotlin title="permutations_ii.kt"
7 months ago
[class]{}-[func]{backtrack}
7 months ago
7 months ago
[class]{}-[func]{permutationsII}
7 months ago
```
=== "Ruby"
```ruby title="permutations_ii.rb"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutations_ii}
```
=== "Zig"
```zig title="permutations_ii.zig"
[class]{}-[func]{backtrack}
[class]{}-[func]{permutationsII}
```
Assuming all elements are distinct from each other, there are $n!$ (factorial) permutations of $n$ elements; when recording results, it is necessary to copy a list of length $n$, using $O(n)$ time. **Thus, the time complexity is $O(n!n)$**.
The maximum recursion depth is $n$, using $O(n)$ frame space. `Selected` uses $O(n)$ space. At any one time, there can be up to $n$ `duplicated`, using $O(n^2)$ space. **Therefore, the space complexity is $O(n^2)$**.
### 3. &nbsp; Comparison of the two pruning methods
Please note, although both `selected` and `duplicated` are used for pruning, their targets are different.
- **Repeated choice pruning**: There is only one `selected` throughout the search process. It records which elements are currently in the state, aiming to prevent an element from appearing repeatedly in `state`.
- **Equal element pruning**: Each round of choices (each call to the `backtrack` function) contains a `duplicated`. It records which elements have been chosen in the current traversal (`for` loop), aiming to ensure equal elements are selected only once.
7 months ago
Figure 13-9 shows the scope of the two pruning conditions. Note, each node in the tree represents a choice, and the nodes from the root to the leaf form a permutation.
7 months ago
![Scope of the two pruning conditions](permutations_problem.assets/permutations_ii_pruning_summary.png){ class="animation-figure" }
<p align="center"> Figure 13-9 &nbsp; Scope of the two pruning conditions </p>