Add the initial translation of chapter "backtracking" (#1323)
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 24 KiB |
@ -0,0 +1,489 @@
|
||||
# Backtracking algorithm
|
||||
|
||||
<u>Backtracking algorithm</u> is a method to solve problems by exhaustive search, where the core idea is to start from an initial state and brute force all possible solutions, recording the correct ones until a solution is found or all possible choices are exhausted without finding a solution.
|
||||
|
||||
Backtracking typically employs "depth-first search" to traverse the solution space. In the "Binary Tree" chapter, we mentioned that preorder, inorder, and postorder traversals are all depth-first searches. Next, we use preorder traversal to construct a backtracking problem to gradually understand the workings of the backtracking algorithm.
|
||||
|
||||
!!! question "Example One"
|
||||
|
||||
Given a binary tree, search and record all nodes with a value of $7$, please return a list of nodes.
|
||||
|
||||
For this problem, we traverse this tree in preorder and check if the current node's value is $7$. If it is, we add the node's value to the result list `res`. The relevant process is shown in the following diagram and code:
|
||||
|
||||
```src
|
||||
[file]{preorder_traversal_i_compact}-[class]{}-[func]{pre_order}
|
||||
```
|
||||
|
||||
![Searching nodes in preorder traversal](backtracking_algorithm.assets/preorder_find_nodes.png)
|
||||
|
||||
## Trying and retreating
|
||||
|
||||
**The reason it is called backtracking is that the algorithm uses a "try" and "retreat" strategy when searching the solution space**. When the algorithm encounters a state where it can no longer progress or fails to achieve a satisfying solution, it undoes the previous choice, reverts to the previous state, and tries other possible choices.
|
||||
|
||||
For Example One, visiting each node represents a "try", and passing a leaf node or returning to the parent node's `return` represents "retreat".
|
||||
|
||||
It's worth noting that **retreat is not merely about function returns**. We expand slightly on Example One for clarification.
|
||||
|
||||
!!! question "Example Two"
|
||||
|
||||
In a binary tree, search for all nodes with a value of $7$ and **please return the paths from the root node to these nodes**.
|
||||
|
||||
Based on the code from Example One, we need to use a list `path` to record the visited node paths. When a node with a value of $7$ is reached, we copy `path` and add it to the result list `res`. After the traversal, `res` holds all the solutions. The code is as shown:
|
||||
|
||||
```src
|
||||
[file]{preorder_traversal_ii_compact}-[class]{}-[func]{pre_order}
|
||||
```
|
||||
|
||||
In each "try", we record the path by adding the current node to `path`; before "retreating", we need to pop the node from `path` **to restore the state before this attempt**.
|
||||
|
||||
Observe the process shown below, **we can understand trying and retreating as "advancing" and "undoing"**, two operations that are reverse to each other.
|
||||
|
||||
=== "<1>"
|
||||
![Trying and retreating](backtracking_algorithm.assets/preorder_find_paths_step1.png)
|
||||
|
||||
=== "<2>"
|
||||
![preorder_find_paths_step2](backtracking_algorithm.assets/preorder_find_paths_step2.png)
|
||||
|
||||
=== "<3>"
|
||||
![preorder_find_paths_step3](backtracking_algorithm.assets/preorder_find_paths_step3.png)
|
||||
|
||||
=== "<4>"
|
||||
![preorder_find_paths_step4](backtracking_algorithm.assets/preorder_find_paths_step4.png)
|
||||
|
||||
=== "<5>"
|
||||
![preorder_find_paths_step5](backtracking_algorithm.assets/preorder_find_paths_step5.png)
|
||||
|
||||
=== "<6>"
|
||||
![preorder_find_paths_step6](backtracking_algorithm.assets/preorder_find_paths_step6.png)
|
||||
|
||||
=== "<7>"
|
||||
![preorder_find_paths_step7](backtracking_algorithm.assets/preorder_find_paths_step7.png)
|
||||
|
||||
=== "<8>"
|
||||
![preorder_find_paths_step8](backtracking_algorithm.assets/preorder_find_paths_step8.png)
|
||||
|
||||
=== "<9>"
|
||||
![preorder_find_paths_step9](backtracking_algorithm.assets/preorder_find_paths_step9.png)
|
||||
|
||||
=== "<10>"
|
||||
![preorder_find_paths_step10](backtracking_algorithm.assets/preorder_find_paths_step10.png)
|
||||
|
||||
=== "<11>"
|
||||
![preorder_find_paths_step11](backtracking_algorithm.assets/preorder_find_paths_step11.png)
|
||||
|
||||
## Pruning
|
||||
|
||||
Complex backtracking problems usually involve one or more constraints, **which are often used for "pruning"**.
|
||||
|
||||
!!! question "Example Three"
|
||||
|
||||
In a binary tree, search for all nodes with a value of $7$ and return the paths from the root to these nodes, **requiring that the paths do not contain nodes with a value of $3**.
|
||||
|
||||
To meet the above constraints, **we need to add a pruning operation**: during the search process, if a node with a value of $3$ is encountered, it returns early, discontinuing further search. The code is as shown:
|
||||
|
||||
```src
|
||||
[file]{preorder_traversal_iii_compact}-[class]{}-[func]{pre_order}
|
||||
```
|
||||
|
||||
"Pruning" is a very vivid noun. As shown in the diagram below, in the search process, **we "cut off" the search branches that do not meet the constraints**, avoiding many meaningless attempts, thus enhancing the search efficiency.
|
||||
|
||||
![Pruning based on constraints](backtracking_algorithm.assets/preorder_find_constrained_paths.png)
|
||||
|
||||
## Framework code
|
||||
|
||||
Next, we attempt to distill the main framework of "trying, retreating, and pruning" from backtracking to enhance the code's universality.
|
||||
|
||||
In the following framework code, `state` represents the current state of the problem, `choices` represents the choices available under the current state:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title=""
|
||||
def backtrack(state: State, choices: list[choice], res: list[state]):
|
||||
"""Backtracking algorithm framework"""
|
||||
# Check if it's a solution
|
||||
if is_solution(state):
|
||||
# Record the solution
|
||||
record_solution(state, res)
|
||||
# Stop searching
|
||||
return
|
||||
# Iterate through all choices
|
||||
for choice in choices:
|
||||
# Pruning: check if the choice is valid
|
||||
if is_valid(state, choice):
|
||||
# Try: make a choice, update the state
|
||||
make_choice(state, choice)
|
||||
backtrack(state, choices, res)
|
||||
# Retreat: undo the choice, revert to the previous state
|
||||
undo_choice(state, choice)
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title=""
|
||||
/* Backtracking algorithm framework */
|
||||
void backtrack(State *state, vector<Choice *> &choices, vector<State *> &res) {
|
||||
// Check if it's a solution
|
||||
if (isSolution(state)) {
|
||||
// Record the solution
|
||||
recordSolution(state, res);
|
||||
// Stop searching
|
||||
return;
|
||||
}
|
||||
// Iterate through all choices
|
||||
for (Choice choice : choices) {
|
||||
// Pruning: check if the choice is valid
|
||||
if (isValid(state, choice)) {
|
||||
// Try: make a choice, update the state
|
||||
makeChoice(state, choice);
|
||||
backtrack(state, choices, res);
|
||||
// Retreat: undo the choice, revert to the previous state
|
||||
undoChoice(state, choice);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title=""
|
||||
/* Backtracking algorithm framework */
|
||||
void backtrack(State state, List<Choice> choices, List<State> res) {
|
||||
// Check if it's a solution
|
||||
if (isSolution(state)) {
|
||||
// Record the solution
|
||||
recordSolution(state, res);
|
||||
// Stop searching
|
||||
return;
|
||||
}
|
||||
// Iterate through all choices
|
||||
for (Choice choice : choices) {
|
||||
// Pruning: check if the choice is valid
|
||||
if (isValid(state, choice)) {
|
||||
// Try: make a choice, update the state
|
||||
makeChoice(state, choice);
|
||||
backtrack(state, choices, res);
|
||||
// Retreat: undo the choice, revert to the previous state
|
||||
undoChoice(state, choice);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title=""
|
||||
/* Backtracking algorithm framework */
|
||||
void Backtrack(State state, List<Choice> choices, List<State> res) {
|
||||
// Check if it's a solution
|
||||
if (IsSolution(state)) {
|
||||
// Record the solution
|
||||
RecordSolution(state, res);
|
||||
// Stop searching
|
||||
return;
|
||||
}
|
||||
// Iterate through all choices
|
||||
foreach (Choice choice in choices) {
|
||||
// Pruning: check if the choice is valid
|
||||
if (IsValid(state, choice)) {
|
||||
// Try: make a choice, update the state
|
||||
MakeChoice(state, choice);
|
||||
Backtrack(state, choices, res);
|
||||
// Retreat: undo the choice, revert to the previous state
|
||||
UndoChoice(state, choice);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title=""
|
||||
/* Backtracking algorithm framework */
|
||||
func backtrack(state *State, choices []Choice, res *[]State) {
|
||||
// Check if it's a solution
|
||||
if isSolution(state) {
|
||||
// Record the solution
|
||||
recordSolution(state, res)
|
||||
// Stop searching
|
||||
return
|
||||
}
|
||||
// Iterate through all choices
|
||||
for _, choice := range choices {
|
||||
// Pruning: check if the choice is valid
|
||||
if isValid(state, choice) {
|
||||
// Try: make a choice, update the state
|
||||
makeChoice(state, choice)
|
||||
backtrack(state, choices, res)
|
||||
// Retreat: undo the choice, revert to the previous state
|
||||
undoChoice(state, choice)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title=""
|
||||
/* Backtracking algorithm framework */
|
||||
func backtrack(state: inout State, choices: [Choice], res: inout [State]) {
|
||||
// Check if it's a solution
|
||||
if isSolution(state: state) {
|
||||
// Record the solution
|
||||
recordSolution(state: state, res: &res)
|
||||
// Stop searching
|
||||
return
|
||||
}
|
||||
// Iterate through all choices
|
||||
for choice in choices {
|
||||
// Pruning: check if the choice is valid
|
||||
if isValid(state: state, choice: choice) {
|
||||
// Try: make a choice, update the state
|
||||
makeChoice(state: &state, choice: choice)
|
||||
backtrack(state: &state, choices: choices, res: &res)
|
||||
// Retreat: undo the choice, revert to the previous state
|
||||
undoChoice(state: &state, choice: choice)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title=""
|
||||
/* Backtracking algorithm framework */
|
||||
function backtrack(state, choices, res) {
|
||||
// Check if it's a solution
|
||||
if (isSolution(state)) {
|
||||
// Record the solution
|
||||
recordSolution(state, res);
|
||||
// Stop searching
|
||||
return;
|
||||
}
|
||||
// Iterate through all choices
|
||||
for (let choice of choices) {
|
||||
// Pruning: check if the choice is valid
|
||||
if (isValid(state, choice)) {
|
||||
// Try: make a choice, update the state
|
||||
makeChoice(state, choice);
|
||||
backtrack(state, choices, res);
|
||||
// Retreat: undo the choice, revert to the previous state
|
||||
undoChoice(state, choice);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title=""
|
||||
/* Backtracking algorithm framework */
|
||||
function backtrack(state: State, choices: Choice[], res: State[]): void {
|
||||
// Check if it's a solution
|
||||
if (isSolution(state)) {
|
||||
// Record the solution
|
||||
recordSolution(state, res);
|
||||
// Stop searching
|
||||
return;
|
||||
}
|
||||
// Iterate through all choices
|
||||
for (let choice of choices) {
|
||||
// Pruning: check if the choice is valid
|
||||
if (isValid(state, choice)) {
|
||||
// Try: make a choice, update the state
|
||||
makeChoice(state, choice);
|
||||
backtrack(state, choices, res);
|
||||
// Retreat: undo the choice, revert to the previous state
|
||||
undoChoice(state, choice);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title=""
|
||||
/* Backtracking algorithm framework */
|
||||
void backtrack(State state, List<Choice>, List<State> res) {
|
||||
// Check if it's a solution
|
||||
if (isSolution(state)) {
|
||||
// Record the solution
|
||||
recordSolution(state, res);
|
||||
// Stop searching
|
||||
return;
|
||||
}
|
||||
// Iterate through all choices
|
||||
for (Choice choice in choices) {
|
||||
// Pruning: check if the choice is valid
|
||||
if (isValid(state, choice)) {
|
||||
// Try: make a choice, update the state
|
||||
makeChoice(state, choice);
|
||||
backtrack(state, choices, res);
|
||||
// Retreat: undo the choice, revert to the previous state
|
||||
undoChoice(state, choice);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title=""
|
||||
/* Backtracking algorithm framework */
|
||||
fn backtrack(state: &mut State, choices: &Vec<Choice>, res: &mut Vec<State>) {
|
||||
// Check if it's a solution
|
||||
if is_solution(state) {
|
||||
// Record the solution
|
||||
record_solution(state, res);
|
||||
// Stop searching
|
||||
return;
|
||||
}
|
||||
// Iterate through all choices
|
||||
for choice in choices {
|
||||
// Pruning: check if the choice is valid
|
||||
if is_valid(state, choice) {
|
||||
// Try: make a choice, update the state
|
||||
make_choice(state, choice);
|
||||
backtrack(state, choices, res);
|
||||
// Retreat: undo the choice, revert to the previous state
|
||||
undo_choice(state, choice);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title=""
|
||||
/* Backtracking algorithm framework */
|
||||
void backtrack(State *state, Choice *choices, int numChoices, State *res, int numRes) {
|
||||
// Check if it's a solution
|
||||
if (isSolution(state)) {
|
||||
// Record the solution
|
||||
recordSolution(state, res, numRes);
|
||||
// Stop searching
|
||||
return;
|
||||
}
|
||||
// Iterate through all choices
|
||||
for (int i = 0; i < numChoices; i++) {
|
||||
// Pruning: check if the choice is valid
|
||||
if (isValid(state, &choices[i])) {
|
||||
// Try: make a choice, update the state
|
||||
makeChoice(state, &choices[i]);
|
||||
backtrack(state, choices, numChoices, res, numRes);
|
||||
// Retreat: undo the choice, revert to the previous state
|
||||
undoChoice(state, &choices[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title=""
|
||||
/* Backtracking algorithm framework */
|
||||
fun backtrack(state: State?, choices: List<Choice?>, res: List<State?>?) {
|
||||
// Check if it's a solution
|
||||
if (isSolution(state)) {
|
||||
// Record the solution
|
||||
recordSolution(state, res)
|
||||
// Stop searching
|
||||
return
|
||||
}
|
||||
// Iterate through all choices
|
||||
for (choice in choices) {
|
||||
// Pruning: check if the choice is valid
|
||||
if (isValid(state, choice)) {
|
||||
// Try: make a choice, update the state
|
||||
makeChoice(state, choice)
|
||||
backtrack(state, choices, res)
|
||||
// Retreat: undo the choice, revert to the previous state
|
||||
undoChoice(state, choice)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title=""
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title=""
|
||||
|
||||
```
|
||||
|
||||
Next, we solve Example Three based on the framework code. The `state` is the node traversal path, `choices` are the current node's left and right children, and the result `res` is the list of paths:
|
||||
|
||||
```src
|
||||
[file]{preorder_traversal_iii_template}-[class]{}-[func]{backtrack}
|
||||
```
|
||||
|
||||
As per the requirements, after finding a node with a value of $7$, the search should continue, **thus the `return` statement after recording the solution should be removed**. The following diagram compares the search processes with and without retaining the `return` statement.
|
||||
|
||||
![Comparison of retaining and removing the return in the search process](backtracking_algorithm.assets/backtrack_remove_return_or_not.png)
|
||||
|
||||
Compared to the implementation based on preorder traversal, the code implementation based on the backtracking algorithm framework seems verbose, but it has better universality. In fact, **many backtracking problems can be solved within this framework**. We just need to define `state` and `choices` according to the specific problem and implement the methods in the framework.
|
||||
|
||||
## Common terminology
|
||||
|
||||
To analyze algorithmic problems more clearly, we summarize the meanings of commonly used terminology in backtracking algorithms and provide corresponding examples from Example Three as shown in the table below.
|
||||
|
||||
<p align="center"> Table <id> Common backtracking algorithm terminology </p>
|
||||
|
||||
| Term | Definition | Example Three |
|
||||
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Solution (solution) | A solution is an answer that satisfies specific conditions of the problem, which may have one or more | All paths from the root node to node $7$ that meet the constraint |
|
||||
| Constraint (constraint) | Constraints are conditions in the problem that limit the feasibility of solutions, often used for pruning | Paths do not contain node $3$ |
|
||||
| State (state) | State represents the situation of the problem at a certain moment, including choices made | Current visited node path, i.e., `path` node list |
|
||||
| Attempt (attempt) | An attempt is the process of exploring the solution space based on available choices, including making choices, updating the state, and checking if it's a solution | Recursively visiting left (right) child nodes, adding nodes to `path`, checking if the node's value is $7$ |
|
||||
| Backtracking (backtracking) | Backtracking refers to the action of undoing previous choices and returning to the previous state when encountering states that do not meet the constraints | When passing leaf nodes, ending node visits, encountering nodes with a value of $3$, terminating the search, and function return |
|
||||
| Pruning (pruning) | Pruning is a method to avoid meaningless search paths based on the characteristics and constraints of the problem, which can enhance search efficiency | When encountering a node with a value of $3$, no further search is continued |
|
||||
|
||||
!!! tip
|
||||
|
||||
Concepts like problems, solutions, states, etc., are universal, and are involved in divide and conquer, backtracking, dynamic programming, and greedy algorithms, among others.
|
||||
|
||||
## Advantages and limitations
|
||||
|
||||
The backtracking algorithm is essentially a depth-first search algorithm that attempts all possible solutions until a satisfying solution is found. The advantage of this method is that it can find all possible solutions, and with reasonable pruning operations, it can be highly efficient.
|
||||
|
||||
However, when dealing with large-scale or complex problems, **the operational efficiency of backtracking may be difficult to accept**.
|
||||
|
||||
- **Time**: Backtracking algorithms usually need to traverse all possible states in the state space, which can reach exponential or factorial time complexity.
|
||||
- **Space**: In recursive calls, it is necessary to save the current state (such as paths, auxiliary variables for pruning, etc.). When the depth is very large, the space requirement may become significant.
|
||||
|
||||
Even so, **backtracking remains the best solution for certain search problems and constraint satisfaction problems**. For these problems, since it is unpredictable which choices can generate valid solutions, we must traverse all possible choices. In this case, **the key is how to optimize efficiency**, with common efficiency optimization methods being two types.
|
||||
|
||||
- **Pruning**: Avoid searching paths that definitely will not produce a solution, thus saving time and space.
|
||||
- **Heuristic search**: Introduce some strategies or estimates during the search process to prioritize the paths that are most likely to produce valid solutions.
|
||||
|
||||
## Typical backtracking problems
|
||||
|
||||
Backtracking algorithms can be used to solve many search problems, constraint satisfaction problems, and combinatorial optimization problems.
|
||||
|
||||
**Search problems**: The goal of these problems is to find solutions that meet specific conditions.
|
||||
|
||||
- Full permutation problem: Given a set, find all possible permutations and combinations of it.
|
||||
- Subset sum problem: Given a set and a target sum, find all subsets of the set that sum to the target.
|
||||
- Tower of Hanoi problem: Given three rods and a series of different-sized discs, the goal is to move all the discs from one rod to another, moving only one disc at a time, and never placing a larger disc on a smaller one.
|
||||
|
||||
**Constraint satisfaction problems**: The goal of these problems is to find solutions that satisfy all the constraints.
|
||||
|
||||
- $n$ queens: Place $n$ queens on an $n \times n$ chessboard so that they do not attack each other.
|
||||
- Sudoku: Fill a $9 \times 9$ grid with the numbers $1$ to $9$, ensuring that the numbers do not repeat in each row, each column, and each $3 \times 3$ subgrid.
|
||||
- Graph coloring problem: Given an undirected graph, color each vertex with the fewest possible colors so that adjacent vertices have different colors.
|
||||
|
||||
**Combinatorial optimization problems**: The goal of these problems is to find the optimal solution within a combination space that meets certain conditions.
|
||||
|
||||
- 0-1 knapsack problem: Given a set of items and a backpack, each item has a certain value and weight. The goal is to choose items to maximize the total value within the backpack's capacity limit.
|
||||
- Traveling salesman problem: In a graph, starting from one point, visit all other points exactly once and then return to the starting point, seeking the shortest path.
|
||||
- Maximum clique problem: Given an undirected graph, find the largest complete subgraph, i.e., a subgraph where any two vertices are connected by an edge.
|
||||
|
||||
Please note that for many combinatorial optimization problems, backtracking is not the optimal solution.
|
||||
|
||||
- The 0-1 knapsack problem is usually solved using dynamic programming to achieve higher time efficiency.
|
||||
- The traveling salesman is a well-known NP-Hard problem, commonly solved using genetic algorithms and ant colony algorithms, among others.
|
||||
- The maximum clique problem is a classic problem in graph theory, which can be solved using greedy algorithms and other heuristic methods.
|
@ -0,0 +1,9 @@
|
||||
# Backtracking
|
||||
|
||||
![Backtracking](../assets/covers/chapter_backtracking.jpg)
|
||||
|
||||
!!! abstract
|
||||
|
||||
Like explorers in a maze, we may encounter difficulties on our path forward.
|
||||
|
||||
The power of backtracking allows us to start over, keep trying, and eventually find the exit to the light.
|
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 7.4 KiB |
@ -0,0 +1,49 @@
|
||||
# n queens problem
|
||||
|
||||
!!! question
|
||||
|
||||
According to the rules of chess, a queen can attack pieces in the same row, column, or on a diagonal line. Given $n$ queens and an $n \times n$ chessboard, find arrangements where no two queens can attack each other.
|
||||
|
||||
As shown in the figure below, when $n = 4$, there are two solutions. From the perspective of the backtracking algorithm, an $n \times n$ chessboard has $n^2$ squares, presenting all possible choices `choices`. The state of the chessboard `state` changes continuously as each queen is placed.
|
||||
|
||||
![Solution to the 4 queens problem](n_queens_problem.assets/solution_4_queens.png)
|
||||
|
||||
The following image shows the three constraints of this problem: **multiple queens cannot be on the same row, column, or diagonal**. It is important to note that diagonals are divided into the main diagonal `\` and the secondary diagonal `/`.
|
||||
|
||||
![Constraints of the n queens problem](n_queens_problem.assets/n_queens_constraints.png)
|
||||
|
||||
### Row-by-row placing strategy
|
||||
|
||||
As the number of queens equals the number of rows on the chessboard, both being $n$, it is easy to conclude: **each row on the chessboard allows and only allows one queen to be placed**.
|
||||
|
||||
This means that we can adopt a row-by-row placing strategy: starting from the first row, place one queen per row until the last row is reached.
|
||||
|
||||
The image below shows the row-by-row placing process for the 4 queens problem. Due to space limitations, the image only expands one search branch of the first row, and prunes any placements that do not meet the column and diagonal constraints.
|
||||
|
||||
![Row-by-row placing strategy](n_queens_problem.assets/n_queens_placing.png)
|
||||
|
||||
Essentially, **the row-by-row placing strategy serves as a pruning function**, avoiding all search branches that would place multiple queens in the same row.
|
||||
|
||||
### Column and diagonal pruning
|
||||
|
||||
To satisfy column constraints, we can use a boolean array `cols` of length $n$ to track whether a queen occupies each column. Before each placement decision, `cols` is used to prune the columns that already have queens, and it is dynamically updated during backtracking.
|
||||
|
||||
How about the diagonal constraints? Let the row and column indices of a cell on the chessboard be $(row, col)$. By selecting a specific main diagonal, we notice that the difference $row - col$ is the same for all cells on that diagonal, **meaning that $row - col$ is a constant value on that diagonal**.
|
||||
|
||||
Thus, if two cells satisfy $row_1 - col_1 = row_2 - col_2$, they are definitely on the same main diagonal. Using this pattern, we can utilize the array `diags1` shown below to track whether a queen is on any main diagonal.
|
||||
|
||||
Similarly, **the sum $row + col$ is a constant value for all cells on a secondary diagonal**. We can also use the array `diags2` to handle secondary diagonal constraints.
|
||||
|
||||
![Handling column and diagonal constraints](n_queens_problem.assets/n_queens_cols_diagonals.png)
|
||||
|
||||
### Code implementation
|
||||
|
||||
Please note, in an $n$-dimensional matrix, the range of $row - col$ is $[-n + 1, n - 1]$, and the range of $row + col$ is $[0, 2n - 2]$, thus the number of both main and secondary diagonals is $2n - 1$, meaning the length of both arrays `diags1` and `diags2` is $2n - 1$.
|
||||
|
||||
```src
|
||||
[file]{n_queens}-[class]{}-[func]{n_queens}
|
||||
```
|
||||
|
||||
Placing $n$ queens row-by-row, considering column constraints, from the first row to the last row there are $n$, $n-1$, $\dots$, $2$, $1$ choices, using $O(n!)$ time. When recording a solution, it is necessary to copy the matrix `state` and add it to `res`, with the copying operation using $O(n^2)$ time. Therefore, **the overall time complexity is $O(n! \cdot n^2)$**. In practice, pruning based on diagonal constraints can significantly reduce the search space, thus often the search efficiency is better than the above time complexity.
|
||||
|
||||
Array `state` uses $O(n^2)$ space, and arrays `cols`, `diags1`, and `diags2` each use $O(n)$ space. The maximum recursion depth is $n$, using $O(n)$ stack space. Therefore, **the space complexity is $O(n^2)$**.
|
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 22 KiB |
@ -0,0 +1,95 @@
|
||||
# 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).
|
||||
|
||||
The table below lists several example data, including the input arrays and their corresponding permutations.
|
||||
|
||||
<p align="center"> Table <id> Permutation examples </p>
|
||||
|
||||
| Input array | Permutations |
|
||||
| :---------- | :----------------------------------------------------------------- |
|
||||
| $[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]$ |
|
||||
|
||||
## 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**.
|
||||
|
||||
As shown in the following figure, 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.
|
||||
|
||||
![Permutation recursive tree](permutations_problem.assets/permutations_i.png)
|
||||
|
||||
### 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.
|
||||
|
||||
As shown in the following figure, 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.
|
||||
|
||||
![Permutation pruning example](permutations_problem.assets/permutations_i_pruning.png)
|
||||
|
||||
Observing the above figure, this pruning operation reduces the search space size from $O(n^n)$ to $O(n!)$.
|
||||
|
||||
### 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:
|
||||
|
||||
```src
|
||||
[file]{permutations_i}-[class]{}-[func]{permutations_i}
|
||||
```
|
||||
|
||||
## 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}$.
|
||||
|
||||
As shown in the following figure, half of the permutations generated by the above method are duplicates.
|
||||
|
||||
![Duplicate permutations](permutations_problem.assets/permutations_ii.png)
|
||||
|
||||
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.
|
||||
|
||||
### Pruning of equal elements
|
||||
|
||||
Observing the following figure, in the first round, choosing $1$ or $\hat{1}$ results in identical permutations under both choices, thus we should prune $\hat{1}$.
|
||||
|
||||
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)
|
||||
|
||||
### 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:
|
||||
|
||||
```src
|
||||
[file]{permutations_ii}-[class]{}-[func]{permutations_ii}
|
||||
```
|
||||
|
||||
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)$**.
|
||||
|
||||
### 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.
|
||||
|
||||
The following figure 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.
|
||||
|
||||
![Scope of the two pruning conditions](permutations_problem.assets/permutations_ii_pruning_summary.png)
|
After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 35 KiB |
After Width: | Height: | Size: 18 KiB |
@ -0,0 +1,95 @@
|
||||
# Subset sum problem
|
||||
|
||||
## Case without duplicate elements
|
||||
|
||||
!!! question
|
||||
|
||||
Given an array of positive integers `nums` and a target positive integer `target`, find all possible combinations such that the sum of the elements in the combination equals `target`. The given array has no duplicate elements, and each element can be chosen multiple times. Please return these combinations as a list, which should not contain duplicate combinations.
|
||||
|
||||
For example, for the input set $\{3, 4, 5\}$ and target integer $9$, the solutions are $\{3, 3, 3\}, \{4, 5\}$. Note the following two points.
|
||||
|
||||
- Elements in the input set can be chosen an unlimited number of times.
|
||||
- Subsets do not distinguish the order of elements, for example $\{4, 5\}$ and $\{5, 4\}$ are the same subset.
|
||||
|
||||
### Reference permutation solution
|
||||
|
||||
Similar to the permutation problem, we can imagine the generation of subsets as a series of choices, updating the "element sum" in real-time during the choice process. When the element sum equals `target`, the subset is recorded in the result list.
|
||||
|
||||
Unlike the permutation problem, **elements in this problem can be chosen an unlimited number of times**, thus there is no need to use a `selected` boolean list to record whether an element has been chosen. We can make minor modifications to the permutation code to initially solve the problem:
|
||||
|
||||
```src
|
||||
[file]{subset_sum_i_naive}-[class]{}-[func]{subset_sum_i_naive}
|
||||
```
|
||||
|
||||
Inputting the array $[3, 4, 5]$ and target element $9$ into the above code yields the results $[3, 3, 3], [4, 5], [5, 4]$. **Although it successfully finds all subsets with a sum of $9$, it includes the duplicate subset $[4, 5]$ and $[5, 4]$**.
|
||||
|
||||
This is because the search process distinguishes the order of choices, however, subsets do not distinguish the choice order. As shown in the following figure, choosing $4$ before $5$ and choosing $5$ before $4$ are different branches, but correspond to the same subset.
|
||||
|
||||
![Subset search and pruning out of bounds](subset_sum_problem.assets/subset_sum_i_naive.png)
|
||||
|
||||
To eliminate duplicate subsets, **a straightforward idea is to deduplicate the result list**. However, this method is very inefficient for two reasons.
|
||||
|
||||
- When there are many array elements, especially when `target` is large, the search process produces a large number of duplicate subsets.
|
||||
- Comparing subsets (arrays) for differences is very time-consuming, requiring arrays to be sorted first, then comparing the differences of each element in the arrays.
|
||||
|
||||
### Duplicate subset pruning
|
||||
|
||||
**We consider deduplication during the search process through pruning**. Observing the following figure, duplicate subsets are generated when choosing array elements in different orders, for example in the following situations.
|
||||
|
||||
1. When choosing $3$ in the first round and $4$ in the second round, all subsets containing these two elements are generated, denoted as $[3, 4, \dots]$.
|
||||
2. Later, when $4$ is chosen in the first round, **the second round should skip $3$** because the subset $[4, 3, \dots]$ generated by this choice completely duplicates the subset from step `1.`.
|
||||
|
||||
In the search process, each layer's choices are tried one by one from left to right, so the more to the right a branch is, the more it is pruned.
|
||||
|
||||
1. First two rounds choose $3$ and $5$, generating subset $[3, 5, \dots]$.
|
||||
2. First two rounds choose $4$ and $5$, generating subset $[4, 5, \dots]$.
|
||||
3. If $5$ is chosen in the first round, **then the second round should skip $3$ and $4$** as the subsets $[5, 3, \dots]$ and $[5, 4, \dots]$ completely duplicate the subsets described in steps `1.` and `2.`.
|
||||
|
||||
![Different choice orders leading to duplicate subsets](subset_sum_problem.assets/subset_sum_i_pruning.png)
|
||||
|
||||
In summary, given the input array $[x_1, x_2, \dots, x_n]$, the choice sequence in the search process should be $[x_{i_1}, x_{i_2}, \dots, x_{i_m}]$, which needs to satisfy $i_1 \leq i_2 \leq \dots \leq i_m$. **Any choice sequence that does not meet this condition will cause duplicates and should be pruned**.
|
||||
|
||||
### Code implementation
|
||||
|
||||
To implement this pruning, we initialize the variable `start`, which indicates the starting point for traversal. **After making the choice $x_{i}$, set the next round to start from index $i$**. This will ensure the choice sequence satisfies $i_1 \leq i_2 \leq \dots \leq i_m$, thereby ensuring the uniqueness of the subsets.
|
||||
|
||||
Besides, we have made the following two optimizations to the code.
|
||||
|
||||
- Before starting the search, sort the array `nums`. In the traversal of all choices, **end the loop directly when the subset sum exceeds `target`** as subsequent elements are larger and their subset sum will definitely exceed `target`.
|
||||
- Eliminate the element sum variable `total`, **by performing subtraction on `target` to count the element sum**. When `target` equals $0$, record the solution.
|
||||
|
||||
```src
|
||||
[file]{subset_sum_i}-[class]{}-[func]{subset_sum_i}
|
||||
```
|
||||
|
||||
The following figure shows the overall backtracking process after inputting the array $[3, 4, 5]$ and target element $9$ into the above code.
|
||||
|
||||
![Subset sum I backtracking process](subset_sum_problem.assets/subset_sum_i.png)
|
||||
|
||||
## Considering cases with duplicate elements
|
||||
|
||||
!!! question
|
||||
|
||||
Given an array of positive integers `nums` and a target positive integer `target`, find all possible combinations such that the sum of the elements in the combination equals `target`. **The given array may contain duplicate elements, and each element can only be chosen once**. Please return these combinations as a list, which should not contain duplicate combinations.
|
||||
|
||||
Compared to the previous question, **this question's input array may contain duplicate elements**, introducing new problems. For example, given the array $[4, \hat{4}, 5]$ and target element $9$, the existing code's output results in $[4, 5], [\hat{4}, 5]$, resulting in duplicate subsets.
|
||||
|
||||
**The reason for this duplication is that equal elements are chosen multiple times in a certain round**. In the following figure, the first round has three choices, two of which are $4$, generating two duplicate search branches, thus outputting duplicate subsets; similarly, the two $4$s in the second round also produce duplicate subsets.
|
||||
|
||||
![Duplicate subsets caused by equal elements](subset_sum_problem.assets/subset_sum_ii_repeat.png)
|
||||
|
||||
### Equal element pruning
|
||||
|
||||
To solve this issue, **we need to limit equal elements to being chosen only once per round**. The implementation is quite clever: since the array is sorted, equal elements are adjacent. This means that in a certain round of choices, if the current element is equal to its left-hand element, it means it has already been chosen, so skip the current element directly.
|
||||
|
||||
At the same time, **this question stipulates that each array element can only be chosen once**. Fortunately, we can also use the variable `start` to meet this constraint: after making the choice $x_{i}$, set the next round to start from index $i + 1$ going forward. This not only eliminates duplicate subsets but also avoids repeated selection of elements.
|
||||
|
||||
### Code implementation
|
||||
|
||||
```src
|
||||
[file]{subset_sum_ii}-[class]{}-[func]{subset_sum_ii}
|
||||
```
|
||||
|
||||
The following figure shows the backtracking process for the array $[4, 4, 5]$ and target element $9$, including four types of pruning operations. Please combine the illustration with the code comments to understand the entire search process and how each type of pruning operation works.
|
||||
|
||||
![Subset sum II backtracking process](subset_sum_problem.assets/subset_sum_ii.png)
|
@ -0,0 +1,23 @@
|
||||
# Summary
|
||||
|
||||
### Key review
|
||||
|
||||
- The essence of the backtracking algorithm is an exhaustive search method, where the solution space is traversed deeply first to find solutions that meet the criteria. During the search, if a satisfying solution is found, it is recorded, until all solutions are found or the search is completed.
|
||||
- The search process of the backtracking algorithm includes trying and retreating. It uses depth-first search to explore various choices, and when a choice does not meet the constraint conditions, the previous choice is undone, reverting to the previous state, and other options are then continued to be tried. Trying and retreating are operations in opposite directions.
|
||||
- Backtracking problems usually contain multiple constraints, which can be used to perform pruning operations. Pruning can terminate unnecessary search branches early, greatly enhancing search efficiency.
|
||||
- Backtracking algorithms are mainly used to solve search problems and constraint satisfaction problems. Although combinatorial optimization problems can be solved using backtracking, there are often more efficient or effective solutions available.
|
||||
- The permutation problem aims to search for all possible permutations of a given set of elements. We use an array to record whether each element has been chosen, cutting off branches that repeatedly select the same element, ensuring each element is selected only once.
|
||||
- In permutation problems, if the set contains duplicate elements, the final result will include duplicate permutations. We need to restrict that identical elements can only be selected once in each round, which is usually implemented using a hash set.
|
||||
- The subset-sum problem aims to find all subsets in a given set that sum to a target value. The set does not distinguish the order of elements, but the search process outputs all ordered results, producing duplicate subsets. Before backtracking, we sort the data and set a variable to indicate the starting point of each round of traversal, thereby pruning the search branches that generate duplicate subsets.
|
||||
- For the subset-sum problem, equal elements in the array can produce duplicate sets. Using the precondition that the array is already sorted, we prune by determining if adjacent elements are equal, thus ensuring equal elements are only selected once per round.
|
||||
- The $n$ queens problem aims to find schemes to place $n$ queens on an $n \times n$ size chessboard in such a way that no two queens can attack each other. The constraints of the problem include row constraints, column constraints, main diagonal constraints, and secondary diagonal constraints. To meet the row constraint, we adopt a strategy of placing one queen per row, ensuring each row has one queen placed.
|
||||
- The handling of column constraints and diagonal constraints is similar. For column constraints, we use an array to record whether there is a queen in each column, thereby indicating whether the selected cell is legal. For diagonal constraints, we use two arrays to respectively record the presence of queens on the main and secondary diagonals; the challenge is in identifying the row and column index patterns that satisfy the same primary (secondary) diagonal.
|
||||
|
||||
### Q & A
|
||||
|
||||
**Q**: How can we understand the relationship between backtracking and recursion?
|
||||
|
||||
Overall, backtracking is a "strategic algorithm," while recursion is more of a "tool."
|
||||
|
||||
- Backtracking algorithms are typically based on recursion. However, backtracking is one of the application scenarios of recursion, specifically in search problems.
|
||||
- The structure of recursion reflects the "sub-problem decomposition" problem-solving paradigm, commonly used in solving problems involving divide and conquer, backtracking, and dynamic programming (memoized recursion).
|