Revisit the English version (#1835)

* Review the English version using Claude-4.5.

* Update mkdocs.yml

* Align the section titles.

* Bug fixes
This commit is contained in:
Yudong Jin
2025-12-30 17:54:01 +08:00
committed by GitHub
parent 091afd38b4
commit 45e1295241
106 changed files with 4195 additions and 3398 deletions
@@ -1,45 +1,45 @@
# Backtracking algorithms
# Backtracking Algorithm
<u>Backtracking algorithm</u> is a method to solve problems by exhaustive search. Its core concept is to start from an initial state and brutally search for all possible solutions. The algorithm records the correct ones until a solution is found or all possible solutions have been tried but no solution can be found.
<u>The backtracking algorithm</u> is a method for solving problems through exhaustive search. Its core idea is to start from an initial state and exhaustively search all possible solutions. When a correct solution is found, it is recorded. This process continues until a solution is found or all possible choices have been tried without finding a solution.
Backtracking typically employs "depth-first search" to traverse the solution space. In the "Binary tree" chapter, we mentioned that pre-order, in-order, and post-order traversals are all depth-first searches. Next, we are going to use pre-order traversal to solve a backtracking problem. This helps us to understand how the algorithm works gradually.
The backtracking algorithm typically employs "depth-first search" to traverse the solution space. In the "Binary Tree" chapter, we mentioned that preorder, inorder, and postorder traversals all belong to depth-first search. Next, we will construct a backtracking problem using preorder traversal to progressively understand how the backtracking algorithm works.
!!! question "Example One"
!!! question "Example 1"
Given a binary tree, search and record all nodes with a value of $7$ and return them in a list.
Given a binary tree, search and record all nodes with value $7$, and return a list of these nodes.
To solve this problem, we traverse this tree in pre-order 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 process is shown in the figure below:
For this problem, we perform a preorder traversal of the tree and check whether the current node's value is $7$. If it is, we add the node to the result list `res`. The relevant implementation is shown in the following figure and code:
```src
[file]{preorder_traversal_i_compact}-[class]{}-[func]{pre_order}
```
![Searching nodes in pre-order traversal](backtracking_algorithm.assets/preorder_find_nodes.png)
![Search for nodes in preorder traversal](backtracking_algorithm.assets/preorder_find_nodes.png)
## Trial and retreat
## Attempt and Backtrack
**It is called a backtracking algorithm because it uses a "trial" and "retreat" strategy when searching the solution space**. During the search, whenever it encounters a state where it can no longer proceed to obtain a satisfying solution, it undoes the previous choice and reverts to the previous state so that other possible choices can be chosen for the next attempt.
**The reason it is called a backtracking algorithm is that it employs "attempt" and "backtrack" strategies when searching the solution space**. When the algorithm encounters a state where it cannot continue forward or cannot find a solution that satisfies the constraints, it will undo the previous choice, return to a previous state, and try other possible choices.
In Example One, visiting each node starts a "trial". And passing a leaf node or the `return` statement to going back to the parent node suggests "retreat".
For Example 1, visiting each node represents an "attempt", while skipping over a leaf node or a function `return` from the parent node represents a "backtrack".
It's worth noting that **retreat is not merely about function returns**. We'll expand slightly on Example One question to explain what it means.
It is worth noting that **backtracking is not limited to function returns alone**. To illustrate this, let's extend Example 1 slightly.
!!! question "Example Two"
!!! question "Example 2"
In a binary tree, search for all nodes with a value of $7$ and for all matching nodes, **please return the paths from the root node to that node**.
In a binary tree, search all nodes with value $7$, **and return the paths from the root node to these nodes**.
Based on the code from Example One, we need to use a list called `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:
Based on the code from Example 1, we need to use a list `path` to record the visited node path. When we reach a node with value $7$, we copy `path` and add it to the result list `res`. After traversal is complete, `res` contains all the solutions. The code is as follows:
```src
[file]{preorder_traversal_ii_compact}-[class]{}-[func]{pre_order}
```
In each "trial", we record the path by adding the current node to `path`. Whenever we need to "retreat", we pop the node from `path` **to restore the state prior to this failed attempt**.
In each "attempt", we record the path by adding the current node to `path`; before "backtracking", we need to remove the node from `path`, **to restore the state before this attempt**.
By observing the process shown in the figure below, **the trial is like "advancing", and retreat is like "undoing"**. The later pairs can be seen as a reverse operation to their counterpart.
Observing the process shown in the following figure, **we can understand attempt and backtrack as "advance" and "undo"**, two operations that are the reverse of each other.
=== "<1>"
![Trying and retreating](backtracking_algorithm.assets/preorder_find_paths_step1.png)
![Attempt and backtrack](backtracking_algorithm.assets/preorder_find_paths_step1.png)
=== "<2>"
![preorder_find_paths_step2](backtracking_algorithm.assets/preorder_find_paths_step2.png)
@@ -71,49 +71,49 @@ By observing the process shown in the figure below, **the trial is like "advanci
=== "<11>"
![preorder_find_paths_step11](backtracking_algorithm.assets/preorder_find_paths_step11.png)
## Prune
## Pruning
Complex backtracking problems usually involve one or more constraints, **which are often used for "pruning"**.
Complex backtracking problems usually contain one or more constraints. **Constraints can typically be used for "pruning"**.
!!! question "Example Three"
!!! question "Example 3"
In a binary tree, search for all nodes with a value of $7$ and return the paths from the root to these nodes, **with restriction that the paths do not contain nodes with a value of $3$**.
In a binary tree, search all nodes with value $7$ and return the paths from the root node to these nodes, **but require that the paths do not contain nodes with value $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 aborts further searching down through the path immediately. The code is as shown:
To satisfy the above constraints, **we need to add pruning operations**: during the search process, if we encounter a node with value $3$, we return early and do not continue searching. The code is as follows:
```src
[file]{preorder_traversal_iii_compact}-[class]{}-[func]{pre_order}
```
"Pruning" is a very vivid noun. As shown in the figure below, in the search process, **we "cut off" the search branches that do not meet the constraints**. It avoids further unnecessary attempts, thus enhances the search efficiency.
"Pruning" is a vivid term. As shown in the following figure, during the search process, **we "prune" search branches that do not satisfy the constraints**, avoiding many meaningless attempts and thus improving search efficiency.
![Pruning based on constraints](backtracking_algorithm.assets/preorder_find_constrained_paths.png)
![Pruning according to constraints](backtracking_algorithm.assets/preorder_find_constrained_paths.png)
## Framework code
## Framework Code
Now, let's try to distill the main framework of "trial, retreat, and prune" from backtracking to enhance the code's universality.
Next, we attempt to extract the main framework of backtracking's "attempt, backtrack, and pruning", to improve code generality.
In the following framework code, `state` represents the current state of the problem, `choices` represents the choices available under the current state:
In the following framework code, `state` represents the current state of the problem, and `choices` represents the choices available in 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
# Check if it is a solution
if is_solution(state):
# Record the solution
record_solution(state, res)
# Stop searching
return
# Iterate through all choices
# Traverse all choices
for choice in choices:
# Prune: check if the choice is valid
# Pruning: check if the choice is valid
if is_valid(state, choice):
# Trial: make a choice, update the state
# Attempt: make a choice and update the state
make_choice(state, choice)
backtrack(state, choices, res)
# Retreat: undo the choice, revert to the previous state
# Backtrack: undo the choice and restore to the previous state
undo_choice(state, choice)
```
@@ -122,21 +122,21 @@ In the following framework code, `state` represents the current state of the pro
```cpp title=""
/* Backtracking algorithm framework */
void backtrack(State *state, vector<Choice *> &choices, vector<State *> &res) {
// Check if it's a solution
// Check if it is a solution
if (isSolution(state)) {
// Record the solution
recordSolution(state, res);
// Stop searching
return;
}
// Iterate through all choices
// Traverse all choices
for (Choice choice : choices) {
// Prune: check if the choice is valid
// Pruning: check if the choice is valid
if (isValid(state, choice)) {
// Trial: make a choice, update the state
// Attempt: make a choice and update the state
makeChoice(state, choice);
backtrack(state, choices, res);
// Retreat: undo the choice, revert to the previous state
// Backtrack: undo the choice and restore to the previous state
undoChoice(state, choice);
}
}
@@ -148,21 +148,21 @@ In the following framework code, `state` represents the current state of the pro
```java title=""
/* Backtracking algorithm framework */
void backtrack(State state, List<Choice> choices, List<State> res) {
// Check if it's a solution
// Check if it is a solution
if (isSolution(state)) {
// Record the solution
recordSolution(state, res);
// Stop searching
return;
}
// Iterate through all choices
// Traverse all choices
for (Choice choice : choices) {
// Prune: check if the choice is valid
// Pruning: check if the choice is valid
if (isValid(state, choice)) {
// Trial: make a choice, update the state
// Attempt: make a choice and update the state
makeChoice(state, choice);
backtrack(state, choices, res);
// Retreat: undo the choice, revert to the previous state
// Backtrack: undo the choice and restore to the previous state
undoChoice(state, choice);
}
}
@@ -174,21 +174,21 @@ In the following framework code, `state` represents the current state of the pro
```csharp title=""
/* Backtracking algorithm framework */
void Backtrack(State state, List<Choice> choices, List<State> res) {
// Check if it's a solution
// Check if it is a solution
if (IsSolution(state)) {
// Record the solution
RecordSolution(state, res);
// Stop searching
return;
}
// Iterate through all choices
// Traverse all choices
foreach (Choice choice in choices) {
// Prune: check if the choice is valid
// Pruning: check if the choice is valid
if (IsValid(state, choice)) {
// Trial: make a choice, update the state
// Attempt: make a choice and update the state
MakeChoice(state, choice);
Backtrack(state, choices, res);
// Retreat: undo the choice, revert to the previous state
// Backtrack: undo the choice and restore to the previous state
UndoChoice(state, choice);
}
}
@@ -200,21 +200,21 @@ In the following framework code, `state` represents the current state of the pro
```go title=""
/* Backtracking algorithm framework */
func backtrack(state *State, choices []Choice, res *[]State) {
// Check if it's a solution
// Check if it is a solution
if isSolution(state) {
// Record the solution
recordSolution(state, res)
// Stop searching
return
}
// Iterate through all choices
// Traverse all choices
for _, choice := range choices {
// Prune: check if the choice is valid
// Pruning: check if the choice is valid
if isValid(state, choice) {
// Trial: make a choice, update the state
// Attempt: make a choice and update the state
makeChoice(state, choice)
backtrack(state, choices, res)
// Retreat: undo the choice, revert to the previous state
// Backtrack: undo the choice and restore to the previous state
undoChoice(state, choice)
}
}
@@ -226,21 +226,21 @@ In the following framework code, `state` represents the current state of the pro
```swift title=""
/* Backtracking algorithm framework */
func backtrack(state: inout State, choices: [Choice], res: inout [State]) {
// Check if it's a solution
// Check if it is a solution
if isSolution(state: state) {
// Record the solution
recordSolution(state: state, res: &res)
// Stop searching
return
}
// Iterate through all choices
// Traverse all choices
for choice in choices {
// Prune: check if the choice is valid
// Pruning: check if the choice is valid
if isValid(state: state, choice: choice) {
// Trial: make a choice, update the state
// Attempt: make a choice and update the state
makeChoice(state: &state, choice: choice)
backtrack(state: &state, choices: choices, res: &res)
// Retreat: undo the choice, revert to the previous state
// Backtrack: undo the choice and restore to the previous state
undoChoice(state: &state, choice: choice)
}
}
@@ -252,21 +252,21 @@ In the following framework code, `state` represents the current state of the pro
```javascript title=""
/* Backtracking algorithm framework */
function backtrack(state, choices, res) {
// Check if it's a solution
// Check if it is a solution
if (isSolution(state)) {
// Record the solution
recordSolution(state, res);
// Stop searching
return;
}
// Iterate through all choices
// Traverse all choices
for (let choice of choices) {
// Prune: check if the choice is valid
// Pruning: check if the choice is valid
if (isValid(state, choice)) {
// Trial: make a choice, update the state
// Attempt: make a choice and update the state
makeChoice(state, choice);
backtrack(state, choices, res);
// Retreat: undo the choice, revert to the previous state
// Backtrack: undo the choice and restore to the previous state
undoChoice(state, choice);
}
}
@@ -278,21 +278,21 @@ In the following framework code, `state` represents the current state of the pro
```typescript title=""
/* Backtracking algorithm framework */
function backtrack(state: State, choices: Choice[], res: State[]): void {
// Check if it's a solution
// Check if it is a solution
if (isSolution(state)) {
// Record the solution
recordSolution(state, res);
// Stop searching
return;
}
// Iterate through all choices
// Traverse all choices
for (let choice of choices) {
// Prune: check if the choice is valid
// Pruning: check if the choice is valid
if (isValid(state, choice)) {
// Trial: make a choice, update the state
// Attempt: make a choice and update the state
makeChoice(state, choice);
backtrack(state, choices, res);
// Retreat: undo the choice, revert to the previous state
// Backtrack: undo the choice and restore to the previous state
undoChoice(state, choice);
}
}
@@ -304,21 +304,21 @@ In the following framework code, `state` represents the current state of the pro
```dart title=""
/* Backtracking algorithm framework */
void backtrack(State state, List<Choice>, List<State> res) {
// Check if it's a solution
// Check if it is a solution
if (isSolution(state)) {
// Record the solution
recordSolution(state, res);
// Stop searching
return;
}
// Iterate through all choices
// Traverse all choices
for (Choice choice in choices) {
// Prune: check if the choice is valid
// Pruning: check if the choice is valid
if (isValid(state, choice)) {
// Trial: make a choice, update the state
// Attempt: make a choice and update the state
makeChoice(state, choice);
backtrack(state, choices, res);
// Retreat: undo the choice, revert to the previous state
// Backtrack: undo the choice and restore to the previous state
undoChoice(state, choice);
}
}
@@ -330,21 +330,21 @@ In the following framework code, `state` represents the current state of the pro
```rust title=""
/* Backtracking algorithm framework */
fn backtrack(state: &mut State, choices: &Vec<Choice>, res: &mut Vec<State>) {
// Check if it's a solution
// Check if it is a solution
if is_solution(state) {
// Record the solution
record_solution(state, res);
// Stop searching
return;
}
// Iterate through all choices
// Traverse all choices
for choice in choices {
// Prune: check if the choice is valid
// Pruning: check if the choice is valid
if is_valid(state, choice) {
// Trial: make a choice, update the state
// Attempt: make a choice and update the state
make_choice(state, choice);
backtrack(state, choices, res);
// Retreat: undo the choice, revert to the previous state
// Backtrack: undo the choice and restore to the previous state
undo_choice(state, choice);
}
}
@@ -356,21 +356,21 @@ In the following framework code, `state` represents the current state of the pro
```c title=""
/* Backtracking algorithm framework */
void backtrack(State *state, Choice *choices, int numChoices, State *res, int numRes) {
// Check if it's a solution
// Check if it is a solution
if (isSolution(state)) {
// Record the solution
recordSolution(state, res, numRes);
// Stop searching
return;
}
// Iterate through all choices
// Traverse all choices
for (int i = 0; i < numChoices; i++) {
// Prune: check if the choice is valid
// Pruning: check if the choice is valid
if (isValid(state, &choices[i])) {
// Trial: make a choice, update the state
// Attempt: make a choice and update the state
makeChoice(state, &choices[i]);
backtrack(state, choices, numChoices, res, numRes);
// Retreat: undo the choice, revert to the previous state
// Backtrack: undo the choice and restore to the previous state
undoChoice(state, &choices[i]);
}
}
@@ -382,21 +382,21 @@ In the following framework code, `state` represents the current state of the pro
```kotlin title=""
/* Backtracking algorithm framework */
fun backtrack(state: State?, choices: List<Choice?>, res: List<State?>?) {
// Check if it's a solution
// Check if it is a solution
if (isSolution(state)) {
// Record the solution
recordSolution(state, res)
// Stop searching
return
}
// Iterate through all choices
// Traverse all choices
for (choice in choices) {
// Prune: check if the choice is valid
// Pruning: check if the choice is valid
if (isValid(state, choice)) {
// Trial: make a choice, update the state
// Attempt: make a choice and update the state
makeChoice(state, choice)
backtrack(state, choices, res)
// Retreat: undo the choice, revert to the previous state
// Backtrack: undo the choice and restore to the previous state
undoChoice(state, choice)
}
}
@@ -406,7 +406,27 @@ In the following framework code, `state` represents the current state of the pro
=== "Ruby"
```ruby title=""
### Backtracking algorithm framework ###
def backtrack(state, choices, res)
# Check if it is a solution
if is_solution?(state)
# Record the solution
record_solution(state, res)
return
end
# Traverse all choices
for choice in choices
# Pruning: check if the choice is valid
if is_valid?(state, choice)
# Attempt: make a choice and update the state
make_choice(state, choice)
backtrack(state, choices, res)
# Backtrack: undo the choice and restore to the previous state
undo_choice(state, choice)
end
end
end
```
=== "Zig"
@@ -415,75 +435,75 @@ In the following framework code, `state` represents the current state of the pro
```
Now, we are able to solve Example Three using 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:
Next, we solve Example 3 based on the framework code. The state `state` is the node traversal path, the choices `choices` are the left and right child nodes of the current node, and the result `res` is a 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. **As a result, the `return` statement after recording the solution should be removed**. The figure below compares the search processes with and without retaining the `return` statement.
As per the problem statement, we should continue searching after finding a node with value $7$. **Therefore, we need to remove the `return` statement after recording the solution**. The following figure compares the search process with and without the `return` statement.
![Comparison of retaining and removing the return in the search process](backtracking_algorithm.assets/backtrack_remove_return_or_not.png)
![Comparison of search process with and without return statement](backtracking_algorithm.assets/backtrack_remove_return_or_not.png)
Compared to the implementation based on pre-order traversal, the code using the backtracking algorithm framework seems verbose. However, 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.
Compared to code based on preorder traversal, code based on the backtracking algorithm framework appears more verbose, but has better generality. In fact, **many backtracking problems can be solved within this framework**. We only need to define `state` and `choices` for the specific problem and implement each method in the framework.
## Common terminology
## 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.
To analyze algorithmic problems more clearly, we summarize the meanings of common terminology used in backtracking algorithms and provide corresponding examples from Example 3, as shown in the following table.
<p align="center"> Table <id> &nbsp; Common backtracking algorithm terminology </p>
<p align="center"> Table <id> &nbsp; Common Backtracking Algorithm Terminology </p>
| Term | Definition | Example Three |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| 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 | Constraints are conditions in the problem that limit the feasibility of solutions, often used for pruning | Paths do not contain node $3$ |
| State | State represents the situation of the problem at a certain moment, including choices made | Current visited node path, i.e., `path` node list |
| Trial | A trial 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$ |
| Retreat | Retreat 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 the recursion function returns |
| Prune | Prune 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 required |
| Term | Definition | Example 3 |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| Solution (solution) | A solution is an answer that satisfies the specific conditions of a problem; there may be one or more solutions | All paths from root to nodes with value $7$ that satisfy the constraint |
| Constraint (constraint) | A constraint is a condition in the problem that limits the feasibility of solutions, typically used for pruning | Paths do not contain nodes with value $3$ |
| State (state) | State represents the situation of a problem at a certain moment, including the choices already made | The currently visited node path, i.e., the `path` list of nodes |
| Attempt (attempt) | An attempt is the process of exploring the solution space according to available choices, including making choices, updating state, and checking if it is a solution | Recursively visit left (right) child nodes, add nodes to `path`, check if node value is $7$ |
| Backtrack (backtracking) | Backtracking refers to undoing previous choices and returning to a previous state when encountering a state that does not satisfy constraints | Stop searching when passing over leaf nodes, ending node visits, or encountering nodes with value $3$; function returns |
| Pruning (pruning) | Pruning is a method of avoiding meaningless search paths according to problem characteristics and constraints, which can improve search efficiency | When encountering a node with value $3$, do not continue searching |
!!! tip
Concepts like problems, solutions, states, etc., are universal, and are involved in divide and conquer, backtracking, dynamic programming, and greedy algorithms, among others.
The concepts of problem, solution, state, etc. are universal and are involved in divide-and-conquer, backtracking, dynamic programming, greedy and other algorithms.
## Advantages and limitations
## 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.
The backtracking algorithm is essentially a depth-first search algorithm that tries all possible solutions until it finds one that satisfies the conditions. The advantage of this approach is that it can find all possible solutions, and with reasonable pruning operations, it achieves high efficiency.
However, when dealing with large-scale or complex problems, **the running efficiency of backtracking algorithm may not be acceptable**.
However, when dealing with large-scale or complex problems, **the running efficiency of the backtracking algorithm may be unacceptable**.
- **Time complexity**: Backtracking algorithms usually need to traverse all possible states in the state space, which can reach exponential or factorial time complexity.
- **Space complexity**: 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 need may become significantly bigger.
- **Time**: The backtracking algorithm usually needs to traverse all possibilities in the solution space, and the time complexity can reach exponential or factorial order.
- **Space**: During recursive calls, the current state needs to be saved (such as paths, auxiliary variables used for pruning, etc.), and when the depth is large, the space requirement can become very large.
Even so, **backtracking remains the best solution for certain search problems and constraint satisfaction problems**. For these problems, there is no way to predict which choices can generate valid solutions. We have to traverse all possible choices. In this case, **the key is about how to optimize the efficiency**. There are two common efficiency optimization methods.
Nevertheless, **the backtracking algorithm is still the best solution for certain search problems and constraint satisfaction problems**. For these problems, since we cannot predict which choices will generate valid solutions, we must traverse all possible choices. In this case, **the key is how to optimize efficiency**. There are two common efficiency optimization methods.
- **Prune**: 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.
- **Pruning**: Avoid searching paths that are guaranteed not to produce solutions, thereby saving time and space.
- **Heuristic search**: Introduce certain strategies or estimation values during the search process to prioritize searching paths that are most likely to produce valid solutions.
## Typical backtracking problems
## Typical Backtracking Examples
Backtracking algorithms can be used to solve many search problems, constraint satisfaction problems, and combinatorial optimization problems.
The backtracking algorithm 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.
**Search problems**: The goal of these problems is to find solutions that satisfy 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.
- Permutation problem: Given a set, find all possible permutations and combinations.
- Subset sum problem: Given a set and a target sum, find all subsets in the set whose elements sum to the target.
- Tower of Hanoi: Given three pegs and a series of disks of different sizes, move all disks from one peg to another, moving only one disk at a time, and never placing a larger disk on a smaller disk.
**Constraint satisfaction problems**: The goal of these problems is to find solutions that satisfy all the constraints.
**Constraint satisfaction problems**: The goal of these problems is to find solutions that satisfy all 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.
- N-Queens: Place $n$ queens on an $n \times n$ chessboard such that they do not attack each other.
- Sudoku: Fill numbers $1$ to $9$ in a $9 \times 9$ grid such that each row, column, and $3 \times 3$ subgrid contains no repeated digits.
- Graph coloring: Given an undirected graph, color each vertex with the minimum number of colors such 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.
**Combinatorial optimization problems**: The goal of these problems is to find an optimal solution that satisfies certain conditions in a combinatorial space.
- 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.
- 0-1 Knapsack: Given a set of items and a knapsack, each item has a value and weight. Under the knapsack capacity constraint, select items to maximize total value.
- Traveling Salesman Problem: Starting from a point in a graph, visit all other points exactly once and return to the starting point, finding the shortest path.
- Maximum Clique: 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.
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.
- The 0-1 Knapsack problem is usually solved using dynamic programming to achieve higher time efficiency.
- The Traveling Salesman Problem is a famous NP-Hard problem; common solutions include genetic algorithms and ant colony algorithms.
- The Maximum Clique problem is a classical problem in graph theory and can be solved using heuristic algorithms such as greedy algorithms.
+2 -2
View File
@@ -4,6 +4,6 @@
!!! abstract
Like explorers in a maze, we may encounter obstacles on our path forward.
We are like explorers in a maze, and may encounter difficulties on the path forward.
The power of backtracking lets us begin anew, keep trying, and eventually find the exit leading to the light.
The power of backtracking allows us to start over, keep trying, and eventually find the exit leading to light.
@@ -1,53 +1,53 @@
# n queens problem
# n-queens problem
!!! question
According to the rules of chess, a queen can attack pieces in the same row, column, or diagonal line. Given $n$ queens and an $n \times n$ chessboard, find arrangements where no two queens can attack each other.
According to the rules of chess, a queen can attack pieces that share the same row, column, or diagonal line. Given $n$ queens and an $n \times n$ chessboard, find a placement scheme such that no two queens can attack each other.
As shown in the figure below, there are two solutions when $n = 4$. 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.
As shown in the figure below, when $n = 4$, there are two solutions that can be found. From the perspective of the backtracking algorithm, an $n \times n$ chessboard has $n^2$ squares, which provide all the choices `choices`. During the process of placing queens one by one, the chessboard state changes continuously, and the chessboard at each moment represents the state `state`.
![Solution to the 4 queens problem](n_queens_problem.assets/solution_4_queens.png)
![Solution to the 4-queens problem](n_queens_problem.assets/solution_4_queens.png)
The figure below shows the three constraints of this problem: **multiple queens cannot occupy the same row, column, or diagonal**. It is important to note that diagonals are divided into the main diagonal `\` and the secondary diagonal `/`.
The figure below illustrates the three constraints of this problem: **multiple queens cannot be in the same row, the same column, or on the same diagonal**. It is worth noting that diagonals are divided into two types: the main diagonal `\` and the anti-diagonal `/`.
![Constraints of the n queens problem](n_queens_problem.assets/n_queens_constraints.png)
![Constraints of the n-queens problem](n_queens_problem.assets/n_queens_constraints.png)
### Row-by-row placing strategy
### Row-by-row placement strategy
As the number of queens equals the number of rows on the chessboard, both being $n$, it is easy to conclude that **each row on the chessboard allows and only allows one queen to be placed**.
Since both the number of queens and the number of rows on the chessboard are $n$, we can easily derive a conclusion: **each row of the chessboard allows and only allows exactly 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.
This means we can adopt a row-by-row placement strategy: starting from the first row, place one queen in each row until the last row is completed.
The figure below shows the row-by-row placing process for the 4 queens problem. Due to space limitations, the figure only expands one search branch of the first row, and prunes any placements that do not meet the column and diagonal constraints.
The figure below shows the row-by-row placement process for the 4-queens problem. Due to space limitations, the figure only expands one search branch of the first row, and all schemes that do not satisfy the column constraint and diagonal constraints are pruned.
![Row-by-row placing strategy](n_queens_problem.assets/n_queens_placing.png)
![Row-by-row placement strategy](n_queens_problem.assets/n_queens_placing.png)
Essentially, **the row-by-row placing strategy serves as a pruning function**, eliminating all search branches that would place multiple queens in the same row.
Essentially, **the row-by-row placement strategy serves a pruning function**, as it avoids all search branches where multiple queens appear 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.
To satisfy the column constraint, we can use a boolean array `cols` of length $n$ to record whether each column has a queen. Before each placement decision, we use `cols` to prune columns that already have queens, and dynamically update the state of `cols` during backtracking.
!!! tip
Note that the origin of the matrix is located in the upper left corner, where the row index increases from top to bottom, and the column index increases from left to right.
Please note that the origin of the matrix is located in the upper-left corner, where the row index increases from top to bottom, and the column index increases from left to right.
How about the diagonal constraints? Let the row and column indices of a certain 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 the main diagonal**.
So how do we handle diagonal constraints? Consider a square on the chessboard with row and column indices $(row, col)$. If we select a specific main diagonal in the matrix, we find that all squares on that diagonal have the same difference between their row and column indices, **meaning that $row - col$ is a constant value for all squares on the main diagonal**.
In other words, 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 in the figure below to track whether a queen is on any main diagonal.
In other words, if two squares satisfy $row_1 - col_1 = row_2 - col_2$, they must be on the same main diagonal. Using this pattern, we can use the array `diags1` shown in the figure below to record whether there is a queen on each main diagonal.
Similarly, **the sum of $row + col$ is a constant value for all cells on the secondary diagonal**. We can also use the array `diags2` to handle secondary diagonal constraints.
Similarly, **for all squares on an anti-diagonal, the sum $row + col$ is a constant value**. We can likewise use the array `diags2` to handle anti-diagonal constraints.
![Handling column and diagonal constraints](n_queens_problem.assets/n_queens_cols_diagonals.png)
### Code implementation
Please note, in an $n$-dimensional square matrix, the range of $row - col$ is $[-n + 1, n - 1]$, and the range of $row + col$ is $[0, 2n - 2]$. Consequently, the number of both main and secondary diagonals is $2n - 1$, meaning the length of the arrays `diags1` and `diags2` is $2n - 1$.
Please note that in an $n$-dimensional square matrix, the range of $row - col$ is $[-n + 1, n - 1]$, and the range of $row + col$ is $[0, 2n - 2]$. Therefore, the number of both main diagonals and anti-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 aforementioned time complexity.
Placing $n$ queens row by row, considering the column constraint, 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`, and the copy operation uses $O(n^2)$ time. Therefore, **the overall time complexity is $O(n! \cdot n^2)$**. In practice, pruning based on diagonal constraints can also significantly reduce the search space, so the search efficiency is often better than the time complexity mentioned above.
Array `state` uses $O(n^2)$ space, and arrays `cols`, `diags1`, and `diags2` each use $O(n)$ space as well. The maximum recursion depth is $n$, using $O(n)$ stack frame space. Therefore, **the space complexity is $O(n^2)$**.
The array `state` uses $O(n^2)$ space, and the arrays `cols`, `diags1`, and `diags2` each use $O(n)$ space. The maximum recursion depth is $n$, using $O(n)$ stack frame space. Therefore, **the space complexity is $O(n^2)$**.
@@ -1,95 +1,95 @@
# Permutation problem
# Permutations Problem
The permutation problem is a typical application of the backtracking algorithm. It involves finding all possible arrangements (permutations) of elements from a given set, such as an array or a string.
The permutations problem is a classic application of backtracking algorithms. It is defined as finding all possible arrangements of elements in a given collection (such as an array or string).
The table below shows several examples, including input arrays and their corresponding permutations.
The table below shows several example datasets, including input arrays and their corresponding permutations.
<p align="center"> Table <id> &nbsp; Permutation examples </p>
<p align="center"> Table <id> &nbsp; Permutations Examples </p>
| Input array | Permutations |
| Input Array | All 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 duplicate elements
## Case with Distinct Elements
!!! question
Given an integer array with no duplicate elements, return all possible permutations.
From a backtracking perspective, **we can view the process of generating permutations as a series of choices.** Suppose the input array is $[1, 2, 3]$. If we choose $1$ first, then $3$, and finally $2$, we get the permutation $[1, 3, 2]$. "Backtracking" means undoing a previous choice and exploring alternative options.
From the perspective of backtracking algorithms, **we can imagine the process of generating permutations as the result of a series of choices**. Suppose the input array is $[1, 2, 3]$. If we first choose $1$, then choose $3$, and finally choose $2$, we obtain the permutation $[1, 3, 2]$. Backtracking means undoing a choice and then trying other choices.
From a coding perspective, the candidate set `choices` consists of all elements in the input array, while `state` holds the elements selected so far. Since each element can only be chosen once, **all elements in `state` must be unique**.
From the perspective of backtracking code, the candidate set `choices` consists of all elements in the input array, and the state `state` is the elements that have been chosen so far. Note that each element can only be chosen once, **therefore all elements in `state` should be unique**.
As illustrated in the figure below, we can expand the search process into a recursive tree, where each node represents the current `state`. Starting from the root node, after three rounds of selections, we reach the leaf nodes—each corresponding to a permutation.
As shown in the figure below, we can unfold the search process into a recursion tree, where each node in the tree represents the current state `state`. Starting from the root node, after three rounds of choices, we reach a leaf node, and each leaf node corresponds to a permutation.
![Permutation recursive tree](permutations_problem.assets/permutations_i.png)
![Recursion tree of permutations](permutations_problem.assets/permutations_i.png)
### Repeated-choice pruning
### Pruning Duplicate Choices
To ensure each element is selected only once, we introduce a boolean array `selected`, where `selected[i]` indicates whether `choices[i]` has been chosen. We then base our pruning steps on this array:
To ensure that each element is chosen only once, we consider introducing a boolean array `selected`, where `selected[i]` indicates whether `choices[i]` has been chosen. We implement the following pruning operation based on it.
- After choosing `choice[i]`, set `selected[i]` to $\text{True}$ to mark it as chosen.
- While iterating through `choices`, skip all elements marked as chosen (i.e., prune those branches).
- After making a choice `choice[i]`, we set `selected[i]` to $\text{True}$, indicating that it has been chosen.
- When traversing the candidate list `choices`, we skip all nodes that have been chosen, which is pruning.
As shown in the figure below, suppose we choose 1 in the first round, then 3 in the second round, and finally 2 in the third round. We need to prune the branch for element 1 in the second round and the branches for elements 1 and 3 in the third round.
As shown in the figure below, suppose we choose $1$ in the first round, $3$ in the second round, and $2$ in the third round. Then we need to prune the branch of element $1$ in the second round and prune the branches of elements $1$ and $3$ in the third round.
![Permutation pruning example](permutations_problem.assets/permutations_i_pruning.png)
![Pruning example of permutations](permutations_problem.assets/permutations_i_pruning.png)
From the figure, we can see that this pruning process reduces the search space from $O(n^n)$ to $O(n!)$.
Observing the above figure, we find that this pruning operation reduces the search space size from $O(n^n)$ to $O(n!)$.
### Code implementation
### Code Implementation
With this understanding, we can "fill in the blanks" of our framework code. To keep the overall code concise, we wont implement each part of the framework separately but instead expand everything in the `backtrack()` function:
After understanding the above information, we can fill in the blanks in the template code. To shorten the overall code, we do not implement each function in the template separately, but instead unfold them in the `backtrack()` function:
```src
[file]{permutations_i}-[class]{}-[func]{permutations_i}
```
## Considering duplicate elements
## Case with Duplicate Elements
!!! question
Given an integer array**that may contain duplicate elements**, return all unique permutations.
Given an integer array that **may contain duplicate elements**, return all unique permutations.
Suppose the input array is $[1, 1, 2]$. To distinguish between the two identical elements $1$, we label the second one as $\hat{1}$.
Suppose the input array is $[1, 1, 2]$. To distinguish the two duplicate elements $1$, we denote the second $1$ as $\hat{1}$.
As shown in the figure below, half of the permutations produced by this method are duplicates:
As shown in the figure below, the method described above generates permutations where half are duplicates.
![Duplicate permutations](permutations_problem.assets/permutations_ii.png)
So how can we eliminate these duplicate permutations? One direct approach is to use a hash set to remove duplicates after generating all permutations. However, this is less elegant **because branches that produce duplicates are inherently unnecessary and should be pruned in advance,** thus improving the algorithms efficiency.
So how do we remove duplicate permutations? The most direct approach is to use a hash set to directly deduplicate the permutation results. However, this is not elegant because **the search branches that generate duplicate permutations are unnecessary and should be identified and pruned early**, which can further improve algorithm efficiency.
### Equal-element pruning
### Pruning Duplicate Elements
Looking at the figure below, in the first round, choosing $1$ or $\hat{1}$ leads to the same permutations, so we prune $\hat{1}$.
Observe the figure below. In the first round, choosing $1$ or choosing $\hat{1}$ is equivalent. All permutations generated under these two choices are duplicates. Therefore, we should prune $\hat{1}$.
Similarly, after choosing $2$ in the first round, choosing $1$ or $\hat{1}$ in the second round also leads to duplicate branches, so we prune $\hat{1}$ then as well.
Similarly, after choosing $2$ in the first round, the $1$ and $\hat{1}$ in the second round also produce duplicate branches, so the second round's $\hat{1}$ should also be pruned.
Essentially, **our goal is to ensure that multiple identical elements are only selected once per round of choices.**
Essentially, **our goal is to ensure that multiple equal elements are chosen only once in a certain round of choices**.
![Duplicate permutations pruning](permutations_problem.assets/permutations_ii_pruning.png)
![Pruning duplicate permutations](permutations_problem.assets/permutations_ii_pruning.png)
### Code implementation
### Code Implementation
Based on the code from the previous problem, we introduce a hash set `duplicated` in each round. This set keeps track of elements we have already attempted, so we can prune duplicates:
Building on the code from the previous problem, we consider opening a hash set `duplicated` in each round of choices to record which elements have been tried in this round, and prune duplicate elements:
```src
[file]{permutations_ii}-[class]{}-[func]{permutations_ii}
```
Assuming all elements are distinct, there are $n!$ (factorial) permutations of $n$ elements. Recording each result requires copying a list of length $n$, which takes $O(n)$ time. **Hence, the total time complexity is $O(n!n)$.**
Assuming elements are pairwise distinct, there are $n!$ (factorial) permutations of $n$ elements. When recording results, we need to copy a list of length $n$, using $O(n)$ time. **Therefore, the time complexity is $O(n! \cdot n)$**.
The maximum recursion depth is $n$, using $O(n)$ stack space. The `selected` array also requires $O(n)$ space. Because there can be up to $n$ separate `duplicated` sets at any one time, they collectively occupy $O(n^2)$ space. **Therefore, the space complexity is $O(n^2)$.**
The maximum recursion depth is $n$, using $O(n)$ stack frame space. `selected` uses $O(n)$ space. At most $n$ `duplicated` sets exist simultaneously, using $O(n^2)$ space. **Therefore, the space complexity is $O(n^2)$**.
### Comparing the two pruning methods
### Comparison of Two Pruning Methods
Although both `selected` and `duplicated` serve as pruning mechanisms, they target different issues:
Note that although both `selected` and `duplicated` are used for pruning, they have different objectives.
- **Repeated-choice pruning**(via `selected`): There is a single `selected` array for the entire search, indicating which elements are already in the current state. This prevents the same element from appearing more than once in `state`.
- **Equal-element pruning**(via `duplicated`): Each call to the `backtrack` function uses its own `duplicated` set, recording which elements have already been chosen in that specific iteration (`for` loop). This ensures that equal elements are selected only once per round of choices.
- **Pruning duplicate choices**: There is only one `selected` throughout the entire search process. It records which elements are included in the current state, and its purpose is to prevent an element from appearing repeatedly in `state`.
- **Pruning duplicate elements**: Each round of choices (each `backtrack` function call) contains a `duplicated` set. It records which elements have been chosen in this round's iteration (the `for` loop), and its purpose is to ensure that equal elements are chosen only once.
The figure below shows the scope of these two pruning strategies. Each node in the tree represents a choice; the path from the root to any leaf corresponds to one complete permutation.
The figure below shows the effective scope of the two pruning conditions. Note that each node in the tree represents a choice, and the nodes on the path from the root to a leaf node form a permutation.
![Scope of the two pruning conditions](permutations_problem.assets/permutations_ii_pruning_summary.png)
![Effective scope of two pruning conditions](permutations_problem.assets/permutations_ii_pruning_summary.png)
@@ -1,95 +1,95 @@
# Subset sum problem
# Subset-Sum Problem
## Case without duplicate elements
## 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.
Given a positive integer array `nums` and a target positive integer `target`, find all possible combinations where the sum of elements in the combination equals `target`. The given array has no duplicate elements, and each element can be selected multiple times. Return these combinations in list form, where the list 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.
For example, given the 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.
- Elements in the input set can be selected repeatedly without limit.
- Subsets do not distinguish element order; for example, $\{4, 5\}$ and $\{5, 4\}$ are the same subset.
### Reference permutation solution
### Reference to Full 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.
Similar to the full permutation problem, we can imagine the process of generating subsets as a series of choices, and update the "sum of elements" in real-time during the selection process. When the sum equals `target`, we record the subset to 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:
Unlike the full permutation problem, **elements in this problem's set can be selected unlimited times**, so we do not need to use a `selected` boolean list to track whether an element has been selected. We can make minor modifications to the full permutation code and initially obtain the solution:
```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]$**.
When we input array $[3, 4, 5]$ and target element $9$ to the above code, the output is $[3, 3, 3], [4, 5], [5, 4]$. **Although we successfully find all subsets that sum to $9$, there are duplicate subsets $[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 figure below, choosing $4$ before $5$ and choosing $5$ before $4$ are different branches, but correspond to the same subset.
This is because the search process distinguishes the order of selections, but subsets do not distinguish selection order. As shown in the figure below, selecting 4 first and then 5 versus selecting 5 first and then 4 are different branches, but they correspond to the same subset.
![Subset search and pruning out of bounds](subset_sum_problem.assets/subset_sum_i_naive.png)
![Subset search and boundary pruning](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.
To eliminate duplicate subsets, **one straightforward idea is to deduplicate the result list**. However, this approach 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.
- When there are many array elements, especially when `target` is large, the search process generates many duplicate subsets.
- Comparing subsets (arrays) is very time-consuming, requiring sorting the arrays first, then comparing each element in them.
### Duplicate subset pruning
### Pruning Duplicate Subsets
**We consider deduplication during the search process through pruning**. Observing the figure below, duplicate subsets are generated when choosing array elements in different orders, for example in the following situations.
**We consider deduplication through pruning during the search process**. Observing the figure below, duplicate subsets occur when array elements are selected in different orders, as in the following cases:
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.`.
1. When the first and second rounds select $3$ and $4$ respectively, all subsets containing these two elements are generated, denoted as $[3, 4, \dots]$.
2. Afterward, when the first round selects $4$, **the second round should skip $3$**, because the subset $[4, 3, \dots]$ generated by this choice is completely duplicate with the subset generated in 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.
In the search process, each level's choices are tried from left to right, so the rightmost branches are pruned more.
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.`.
1. The first two rounds select $3$ and $5$, generating subset $[3, 5, \dots]$.
2. The first two rounds select $4$ and $5$, generating subset $[4, 5, \dots]$.
3. If the first round selects $5$, **the second round should skip $3$ and $4$**, because subsets $[5, 3, \dots]$ and $[5, 4, \dots]$ are completely duplicate with the subsets described in steps `1.` and `2.`
![Different choice orders leading to duplicate subsets](subset_sum_problem.assets/subset_sum_i_pruning.png)
![Different selection 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**.
In summary, given an input array $[x_1, x_2, \dots, x_n]$, let the selection sequence in the search process be $[x_{i_1}, x_{i_2}, \dots, x_{i_m}]$. This selection sequence must satisfy $i_1 \leq i_2 \leq \dots \leq i_m$; **any selection sequence that does not satisfy this condition will cause duplicates and should be pruned**.
### Code implementation
### 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.
To implement this pruning, we initialize a variable `start` to indicate the starting point of traversal. **After making choice $x_{i}$, set the next round to start traversal from index $i$**. This ensures that the selection sequence satisfies $i_1 \leq i_2 \leq \dots \leq i_m$, guaranteeing subset uniqueness.
Besides, we have made the following two optimizations to the code.
In addition, 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.
- Before starting the search, first sort the array `nums`. When traversing all choices, **end the loop immediately when the subset sum exceeds `target`**, because subsequent elements are larger, and their subset sums must exceed `target`.
- Omit the element sum variable `total` and **use subtraction on `target` to track the sum of elements**. Record the solution when `target` equals $0$.
```src
[file]{subset_sum_i}-[class]{}-[func]{subset_sum_i}
```
The figure below shows the overall backtracking process after inputting the array $[3, 4, 5]$ and target element $9$ into the above code.
The figure below shows the complete backtracking process when array $[3, 4, 5]$ and target element $9$ are input to the above code.
![Subset sum I backtracking process](subset_sum_problem.assets/subset_sum_i.png)
![Subset-sum I backtracking process](subset_sum_problem.assets/subset_sum_i.png)
## Considering cases with duplicate elements
## With Duplicate Elements in Array
!!! 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.
Given a positive integer array `nums` and a target positive integer `target`, find all possible combinations where the sum of elements in the combination equals `target`. **The given array may contain duplicate elements, and each element can be selected at most once**. Return these combinations in list form, where the list 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.
Compared to the previous problem, **the input array in this problem may contain duplicate elements**, which introduces new challenges. For example, given array $[4, \hat{4}, 5]$ and target element $9$, the output of the existing code is $[4, 5], [\hat{4}, 5]$, which contains duplicate subsets.
**The reason for this duplication is that equal elements are chosen multiple times in a certain round**. In the figure below, 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.
**The reason for this duplication is that equal elements are selected multiple times in a certain round**. In the figure below, the first round has three choices, two of which are $4$, creating two duplicate search branches that output 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
### Pruning Equal Elements
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.
To solve this problem, **we need to limit equal elements to be selected only once in each round**. The implementation is quite clever: since the array is already sorted, equal elements are adjacent. This means that in a certain round of selection, if the current element equals the element to its left, it means this element has already been selected, so we 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.
At the same time, **this problem specifies that each array element can only be selected once**. Fortunately, we can also use the variable `start` to satisfy this constraint: after making choice $x_{i}$, set the next round to start traversal from index $i + 1$ onwards. This both eliminates duplicate subsets and avoids selecting elements multiple times.
### Code implementation
### Code Implementation
```src
[file]{subset_sum_ii}-[class]{}-[func]{subset_sum_ii}
```
The figure below 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.
The figure below shows the backtracking process for array $[4, 4, 5]$ and target element $9$, which includes four types of pruning operations. Combine the illustration with the code comments to understand the entire search process and how each pruning operation works.
![Subset sum II backtracking process](subset_sum_problem.assets/subset_sum_ii.png)
![Subset-sum II backtracking process](subset_sum_problem.assets/subset_sum_ii.png)
+15 -15
View File
@@ -1,23 +1,23 @@
# Summary
### Key review
### Key Review
- The essence of the backtracking algorithm is exhaustive search. It seeks solutions that meet the conditions by performing a depth-first traversal of the solution space. During the search, if a satisfying solution is found, it is recorded, until all solutions are found or the traversal is completed.
- The search process of the backtracking algorithm includes trying and backtracking. It uses depth-first search to explore various choices, and when a choice does not meet the constraints, the previous choice is undone. Then it reverts to the previous state and continues to try other options. Trying and backtracking are operations in opposite directions.
- Backtracking problems usually contain multiple constraints. These constraints can be used to perform pruning operations. Pruning can terminate unnecessary search branches in advance, greatly enhancing search efficiency.
- The backtracking algorithm is 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 the elements in a given set. We use an array to record whether each element has been chosen, avoiding repeated selection of the same element. This ensures that each element is chosen 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 may generate duplicate subsets. This occurs because the algorithm explores different element orders as unique paths. Before backtracking, we sort the data and set a variable to indicate the starting point of the traversal for each round. This allows us to prune 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. This ensures that 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$ chessboard such that no two queens can attack each other. The constraints of the problem include row constraints, column constraints, and constraints on the main and secondary diagonals. 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 to determine the relationship between row and column indices for cells on the same main or secondary diagonal.
- The backtracking algorithm is fundamentally an exhaustive search method. It finds solutions that meet specified conditions by performing a depth-first traversal of the solution space. During the search process, when a solution satisfying the conditions is found, it is recorded. The search ends either after finding all solutions or when the traversal is complete.
- The backtracking algorithm search process consists of two parts: attempting and backtracking. It tries various choices through depth-first search. When encountering situations that violate constraints, it reverts the previous choice, returns to the previous state, and continues exploring other options. Attempting and backtracking are operations in opposite directions.
- Backtracking problems typically contain multiple constraints, which can be utilized to implement pruning operations. Pruning can terminate unnecessary search branches early, significantly improving search efficiency.
- The backtracking algorithm is primarily used to solve search problems and constraint satisfaction problems. While combinatorial optimization problems can be solved with backtracking, there are often more efficient or better-performing solutions available.
- The permutation problem aims to find all possible permutations of elements in a given set. We use an array to record whether each element has been selected, thereby pruning search branches that attempt to select the same element repeatedly, ensuring each element is selected exactly once.
- In the permutation problem, if the set contains duplicate elements, the final result will contain duplicate permutations. We need to impose a constraint so that equal elements can only be selected once per round, which is typically achieved using a hash set.
- The subset-sum problem aims to find all subsets of a given set that sum to a target value. Since the set is unordered but the search process outputs results in all orders, duplicate subsets are generated. We sort the data before backtracking and use a variable to indicate the starting point of each round's traversal, thereby pruning search branches that generate duplicate subsets.
- For the subset-sum problem, equal elements in the array produce duplicate sets. We leverage the precondition that the array is sorted by checking whether adjacent elements are equal to implement pruning, ensuring that equal elements can only be selected once per round.
- The $n$ queens problem aims to find placements of $n$ queens on an $n \times n$ chessboard such that no two queens can attack each other. The constraints of this problem include row constraints, column constraints, and main and anti-diagonal constraints. To satisfy row constraints, we adopt a row-by-row placement strategy, ensuring exactly one queen is placed in each row.
- The handling of column constraints and diagonal constraints is similar. For column constraints, we use an array to record whether each column has a queen, thereby indicating whether a selected cell is valid. For diagonal constraints, we use two arrays to separately record whether queens exist on each main or anti-diagonal. The challenge lies in finding the row-column index pattern that characterizes cells on the same main (anti-)diagonal.
### Q & A
**Q**: How can we understand the relationship between backtracking and recursion?
**Q**: How should we understand the relationship between backtracking and recursion?
Overall, backtracking is an "algorithmic strategy," while recursion is more of a "tool."
Overall, backtracking is an "algorithm strategy", while recursion is more like 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 problem-solving paradigm of "sub-problem decomposition." It is commonly used in solving problems involving divide and conquer, backtracking, and dynamic programming (memoized recursion).
- The backtracking algorithm is typically implemented based on recursion. However, backtracking is one application scenario of recursion and represents the application of recursion in search problems.
- The structure of recursion embodies the "subproblem decomposition" problem-solving paradigm, commonly used to solve problems involving divide-and-conquer, backtracking, and dynamic programming (memoized recursion).