First version.

This commit is contained in:
krahets
2026-01-20 15:08:42 +08:00
parent 2213a59ff6
commit 8071daddaa
106 changed files with 11790 additions and 0 deletions
@@ -0,0 +1,503 @@
# Поиск с возвратом
<u>Алгоритм поиска с возвратом (backtracking algorithm)</u> -- это метод решения задач путем перебора. Его основная идея заключается в том, чтобы, начиная с начального состояния, осуществлять грубый поиск всех возможных решений, фиксируя правильное найденное решение. Процесс поиска продолжается до тех пор, пока не будет найдено решение или не будут исчерпаны все возможные варианты.
Алгоритмы поиска с возвратом обычно используют поиск в глубину для обхода пространства решений. В разделе «Двоичные деревья» упоминалось, что прямой, симметричный и обратный обходы относятся к поиску в глубину. Далее, используя прямой обход, мы реализуем задачу поиска с возвратом, чтобы постепенно понять принцип работы этого алгоритма.
!!! question "Пример 1"
Дано двоичное дерево, найти и записать все узлы со значением $7$, вернуть список узлов.
Для решения этой задачи мы выполняем предварительный обход дерева и проверяем, равно ли значение текущего узла $7$. Если равно, то добавляем значение этого узла в список результатов `res`. Процесс представлен на следующем рисунке и в коде ниже:
```src
[file]{preorder_traversal_i_compact}-[class]{}-[func]{pre_order}
```
![Поиск узлов в предварительном обходе](../assets/preorder_find_nodes.png)
## Попытка и возврат
**Алгоритм называется поиском с возвратом, потому что при поиске в пространстве решений он использует стратегию попытки и возврата**. Когда алгоритм сталкивается с состоянием, в котором невозможно продолжать или невозможно получить удовлетворительное решение, он отменяет предыдущий выбор, возвращается к предыдущему состоянию и пробует другие возможные варианты.
В примере 1 посещение каждого узла представляет собой попытку, а переход через листовой узел или возврат к родительскому узлу через `return` означает возврат.
Стоит отметить, что **откат включает не только возврат функции**. Чтобы объяснить это, мы немного расширим пример 1.
!!! question "Пример 2"
В двоичном дереве найти все узлы со значением $7$, **вернуть пути от корневого узла до этих узлов**.
Возьмем за основу код для примера 1. Нам потребуется добавить список `path` для записи пути посещенных узлов. Когда будет найден узел со значением $7$, скопируем `path` и добавим его в список результатов `res`. После завершения обхода `res` будет содержать все решения. Код реализации представлен ниже:
```src
[file]{preorder_traversal_ii_compact}-[class]{}-[func]{pre_order}
```
В каждой попытке мы добавляем текущий узел в `path` для записи пути. Перед возвратом необходимо удалить этот узел из `path`, чтобы **восстановить состояние до этой попытки**.
Изучив процесс выполнения алгоритма на следующем рисунке, **можно представить попытку и возврат как движение вперед и отмену**, как два противоположных действия.
=== "<1>"
![Попытка и возврат](../assets/preorder_find_paths_step1.png)
=== "<2>"
![preorder_find_paths_step2](../assets/preorder_find_paths_step2.png)
=== "<3>"
![preorder_find_paths_step3](../assets/preorder_find_paths_step3.png)
=== "<4>"
![preorder_find_paths_step4](../assets/preorder_find_paths_step4.png)
=== "<5>"
![preorder_find_paths_step5](../assets/preorder_find_paths_step5.png)
=== "<6>"
![preorder_find_paths_step6](../assets/preorder_find_paths_step6.png)
=== "<7>"
![preorder_find_paths_step7](../assets/preorder_find_paths_step7.png)
=== "<8>"
![preorder_find_paths_step8](../assets/preorder_find_paths_step8.png)
=== "<9>"
![preorder_find_paths_step9](../assets/preorder_find_paths_step9.png)
=== "<10>"
![preorder_find_paths_step10](../assets/preorder_find_paths_step10.png)
=== "<11>"
![preorder_find_paths_step11](../assets/preorder_find_paths_step11.png)
## Обрезка
Сложные задачи поиска с возвратом обычно содержат одно или несколько ограничений, **которые можно использовать для обрезки**.
!!! question "Пример 3"
В двоичном дереве найти все узлы со значением $7$, вернуть пути от корневого узла до этих узлов, **при этом путь не должен содержать узлы со значением $3$**.
Для выполнения данного условия **требуется добавить операцию обрезки**: в процессе поиска, если встречается узел со значением $3$, следует немедленно вернуться, не продолжая поиск. Код реализации представлен ниже:
```src
[file]{preorder_traversal_iii_compact}-[class]{}-[func]{pre_order}
```
Обрезка является очень наглядным термином. В процессе поиска, как показано на следующем рисунке, **мы обрезаем ветви поиска, не удовлетворяющие заданным условиям**, и избегаем множества бессмысленных попыток, тем самым повышая эффективность поиска.
![Обрезка в соответствии с заданными условиями](../assets/preorder_find_constrained_paths.png)
## Каркас кода
Далее мы попытаемся сформировать основной каркас операций «попытка, возврат, обрезка» для повышения универсальности кода.
В следующем каркасе кода `state` обозначает текущее состояние задачи, а `choices` -- возможные выборы в текущем состоянии:
=== "Python"
```python title=""
def backtrack(state: State, choices: list[choice], res: list[state]):
"""Каркас алгоритма поиска с возвратом"""
# Проверка, является ли состояние решением
if is_solution(state):
# Запись решения
record_solution(state, res)
# Не продолжать поиск
return
# Перебор всех вариантов
for choice in choices:
# Обрезка: проверка легитимности выбора
if is_valid(state, choice):
# Попытка: сделать выбор, обновить состояние
make_choice(state, choice)
backtrack(state, choices, res)
# Возврат: отмена выбора, возврат к предыдущему состоянию
undo_choice(state, choice)
```
=== "C++"
```cpp title=""
/* Каркас алгоритма поиска с возвратом */
void backtrack(State *state, vector<Choice *> &choices, vector<State *> &res) {
// Проверка, является ли состояние решением
if (isSolution(state)) {
// Запись решения
recordSolution(state, res);
// Не продолжать поиск
return;
}
// Перебор всех вариантов
for (Choice choice : choices) {
// Обрезка: проверка легитимности выбора
if (isValid(state, choice)) {
// Попытка: сделать выбор, обновить состояние
makeChoice(state, choice);
backtrack(state, choices, res);
// Возврат: отмена выбора, возврат к предыдущему состоянию
undoChoice(state, choice);
}
}
}
```
=== "Java"
```java title=""
/* Каркас алгоритма поиска с возвратом */
void backtrack(State state, List<Choice> choices, List<State> res) {
// Проверка, является ли состояние решением
if (isSolution(state)) {
// Запись решения
recordSolution(state, res);
// Не продолжать поиск
return;
}
// Перебор всех вариантов
for (Choice choice : choices) {
// Обрезка: проверка легитимности выбора
if (isValid(state, choice)) {
// Попытка: сделать выбор, обновить состояние
makeChoice(state, choice);
backtrack(state, choices, res);
// Возврат: отмена выбора, возврат к предыдущему состоянию
undoChoice(state, choice);
}
}
}
```
=== "C#"
```csharp title=""
/* Каркас алгоритма поиска с возвратом */
void Backtrack(State state, List<Choice> choices, List<State> res) {
// Проверка, является ли состояние решением
if (IsSolution(state)) {
// Запись решения
RecordSolution(state, res);
// Не продолжать поиск
return;
}
// Перебор всех вариантов
foreach (Choice choice in choices) {
// Обрезка: проверка легитимности выбора
if (IsValid(state, choice)) {
// Попытка: сделать выбор, обновить состояние
MakeChoice(state, choice);
Backtrack(state, choices, res);
// Возврат: отмена выбора, возврат к предыдущему состоянию
UndoChoice(state, choice);
}
}
}
```
=== "Go"
```go title=""
/* Каркас алгоритма поиска с возвратом */
func backtrack(state *State, choices []Choice, res *[]State) {
// Проверка, является ли состояние решением
if isSolution(state) {
// Запись решения
recordSolution(state, res)
// Не продолжать поиск
return
}
// Перебор всех вариантов
for _, choice := range choices {
// Обрезка: проверка легитимности выбора
if isValid(state, choice) {
// Попытка: сделать выбор, обновить состояние
makeChoice(state, choice)
backtrack(state, choices, res)
// Возврат: отмена выбора, возврат к предыдущему состоянию
undoChoice(state, choice)
}
}
}
```
=== "Swift"
```swift title=""
/* Каркас алгоритма поиска с возвратом */
func backtrack(state: inout State, choices: [Choice], res: inout [State]) {
// Проверка, является ли состояние решением
if isSolution(state: state) {
// Запись решения
recordSolution(state: state, res: &res)
// Не продолжать поиск
return
}
// Перебор всех вариантов
for choice in choices {
// Обрезка: проверка легитимности выбора
if isValid(state: state, choice: choice) {
// Попытка: сделать выбор, обновить состояние
makeChoice(state: &state, choice: choice)
backtrack(state: &state, choices: choices, res: &res)
// Возврат: отмена выбора, возврат к предыдущему состоянию
undoChoice(state: &state, choice: choice)
}
}
}
```
=== "JS"
```javascript title=""
/* Каркас алгоритма поиска с возвратом */
function backtrack(state, choices, res) {
// Проверка, является ли состояние решением
if (isSolution(state)) {
// Запись решения
recordSolution(state, res);
// Не продолжать поиск
return;
}
// Перебор всех вариантов
for (let choice of choices) {
// Обрезка: проверка легитимности выбора
if (isValid(state, choice)) {
// Попытка: сделать выбор, обновить состояние
makeChoice(state, choice);
backtrack(state, choices, res);
// Возврат: отмена выбора, возврат к предыдущему состоянию
undoChoice(state, choice);
}
}
}
```
=== "TS"
```typescript title=""
/* Каркас алгоритма поиска с возвратом */
function backtrack(state: State, choices: Choice[], res: State[]): void {
// Проверка, является ли состояние решением
if (isSolution(state)) {
// Запись решения
recordSolution(state, res);
// Не продолжать поиск
return;
}
// Перебор всех вариантов
for (let choice of choices) {
// Обрезка: проверка легитимности выбора
if (isValid(state, choice)) {
// Попытка: сделать выбор, обновить состояние
makeChoice(state, choice);
backtrack(state, choices, res);
// Возврат: отмена выбора, возврат к предыдущему состоянию
undoChoice(state, choice);
}
}
}
```
=== "Dart"
```dart title=""
/* Каркас алгоритма поиска с возвратом */
void backtrack(State state, List<Choice>, List<State> res) {
// Проверка, является ли состояние решением
if (isSolution(state)) {
// Запись решения
recordSolution(state, res);
// Не продолжать поиск
return;
}
// Перебор всех вариантов
for (Choice choice in choices) {
// Обрезка: проверка легитимности выбора
if (isValid(state, choice)) {
// Попытка: сделать выбор, обновить состояние
makeChoice(state, choice);
backtrack(state, choices, res);
// Возврат: отмена выбора, возврат к предыдущему состоянию
undoChoice(state, choice);
}
}
}
```
=== "Rust"
```rust title=""
/* Каркас алгоритма поиска с возвратом */
fn backtrack(state: &mut State, choices: &Vec<Choice>, res: &mut Vec<State>) {
// Проверка, является ли состояние решением
if is_solution(state) {
// Запись решения
record_solution(state, res);
// Не продолжать поиск
return;
}
// Перебор всех вариантов
for choice in choices {
// Обрезка: проверка легитимности выбора
if is_valid(state, choice) {
// Попытка: сделать выбор, обновить состояние
make_choice(state, choice);
backtrack(state, choices, res);
// Возврат: отмена выбора, возврат к предыдущему состоянию
undo_choice(state, choice);
}
}
}
```
=== "C"
```c title=""
/* Каркас алгоритма поиска с возвратом */
void backtrack(State *state, Choice *choices, int numChoices, State *res, int numRes) {
// Проверка, является ли состояние решением
if (isSolution(state)) {
// Запись решения
recordSolution(state, res, numRes);
// Не продолжать поиск
return;
}
// Перебор всех вариантов
for (int i = 0; i < numChoices; i++) {
// Обрезка: проверка легитимности выбора
if (isValid(state, &choices[i])) {
// Попытка: сделать выбор, обновить состояние
makeChoice(state, &choices[i]);
backtrack(state, choices, numChoices, res, numRes);
// Возврат: отмена выбора, возврат к предыдущему состоянию
undoChoice(state, &choices[i]);
}
}
}
```
=== "Kotlin"
```kotlin title=""
/* Каркас алгоритма поиска с возвратом */
fun backtrack(state: State?, choices: List<Choice?>, res: List<State?>?) {
// Проверка, является ли состояние решением
if (isSolution(state)) {
// Запись решения
recordSolution(state, res)
// Не продолжать поиск
return
}
// Перебор всех вариантов
for (choice in choices) {
// Обрезка: проверка легитимности выбора
if (isValid(state, choice)) {
// Попытка: сделать выбор, обновить состояние
makeChoice(state, choice)
backtrack(state, choices, res)
// Возврат: отмена выбора, возврат к предыдущему состоянию
undoChoice(state, choice)
}
}
}
```
=== "Ruby"
```ruby title=""
### Каркас алгоритма поиска с возвратом ###
def backtrack(state, choices, res)
# Проверка, является ли состояние решением
if is_solution?(state)
# Запись решения
record_solution(state, res)
return
end
# Перебор всех вариантов
for choice in choices
# Обрезка: проверка легитимности выбора
if is_valid?(state, choice)
# Попытка: сделать выбор, обновить состояние
make_choice(state, choice)
backtrack(state, choices, res)
# Возврат: отмена выбора, возврат к предыдущему состоянию
undo_choice(state, choice)
end
end
end
```
Теперь на основе каркаса кода решим пример 3. Состояние `state` -- это путь обхода узлов, выбор `choices` -- это левый и правый дочерние узлы текущего узла, результат `res` -- список путей:
```src
[file]{preorder_traversal_iii_template}-[class]{}-[func]{backtrack}
```
Согласно условию задачи после нахождения узла со значением $7$ необходимо продолжать поиск, **поэтому следует удалить оператор `return` после записи решения**. На следующем рисунке сравнивается процесс поиска с сохранением и удалением оператора `return`.
![Сравнение процесса поиска с сохранением и удалением return](../assets/backtrack_remove_return_or_not.png)
По сравнению с реализацией на основе предварительного обхода, реализация на основе каркаса поиска с возвратом выглядит более громоздкой, но обладает большей универсальностью. На самом деле **многие задачи поиска с возвратом можно решить в рамках этого каркаса**. Необходимо лишь определить `state` и `choices` в соответствии с конкретной задачей и реализовать методы каркаса.
## Основные термины
Для более четкого понимания алгоритмических задач мы систематизируем значения часто используемых терминов обратного поиска и приведем соответствующие примеры для задачи 3, как показано в следующей таблице.
<p align="center"> Таблица <id> &nbsp; Основные термины обратного поиска </p>
| Термин | Определение | Пример 3 |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Решение (solution) | Ответ, удовлетворяющий определенным условиям задачи, может быть одно или несколько решений | Все пути от корневого узла до узла $7$, удовлетворяющие условиям |
| Ограничение (constraint) | Ограничение на допустимость решения, обычно используется для обрезки | Путь не содержит узлов со значением $3$ |
| Состояние (state) | Ситуация задачи в определенный момент, включая сделанные выборы | Текущий посещенный путь узлов, т. е. список узлов `path` |
| Попытка (attempt) | Процесс исследования пространства решений на основе доступных выборов, включая выбор, обновление состояния, проверку на решение | Рекурсивный доступ к левому (правому) дочернему узлу, добавление узла в `path`, проверка значения узла на равенство $7$ |
| Возврат (backtracking) | Отмена предыдущих выборов и возврат к предыдущему состоянию при встрече состояния, не удовлетворяющего ограничению | При переходе через листовой узел, завершении посещения узла, встрече узла со значением $3$ поиск прекращается, происходит выход из функции |
| Обрезка (pruning) | Метод избежания бессмысленных путей поиска на основе особенностей задачи и ограничений, может повысить эффективность поиска | При встрече узла со значением $3$ не продолжать поиск |
!!! tip
Понятия задачи, решения, состояния и т. д. являются универсальными и встречаются в алгоритмах «разделяй и властвуй», поиска с возвратом, динамического программирования, жадных алгоритмах и других.
## Преимущества и ограничения
Алгоритм поиска с возвратом по своей сути является алгоритмом поиска в глубину, который пытается найти все возможные решения до тех пор, пока не будет найдено удовлетворяющее условиям решение. Преимущество этого метода заключается в том, что он может найти все возможные решения, и при разумной обрезке обладает высокой эффективностью.
Однако при обработке крупномасштабных или сложных задач **эффективность работы алгоритма поиска с возвратом может быть неприемлемой**.
- **Время**: Алгоритм поиска с возвратом обычно требует обхода всех возможных состояний пространства состояний, временная сложность может достигать экспоненциального или факториального порядка.
- **Пространство**: При рекурсивных вызовах необходимо сохранять текущее состояние (например, путь, вспомогательные переменные для обрезки и т. д.), когда глубина очень велика, требования к пространству могут стать очень большими.
Несмотря на это, **алгоритм поиска с возвратом остается лучшим решением для некоторых задач поиска и задач удовлетворения ограничений**. Для этих задач, поскольку невозможно предсказать, какие выборы приведут к правильному решению, необходимо перебрать все возможные варианты. В этом случае **ключевым является оптимизация эффективности**, обычно используются два метода оптимизации эффективности.
- **Обрезка**: Избежание поиска путей, которые определенно не приведут к решению, тем самым экономя время и пространство.
- **Эвристический поиск**: Введение некоторых стратегий или оценочных значений в процессе поиска для приоритетного поиска путей, которые с наибольшей вероятностью приведут к правильному решению.
## Типичные примеры задач поиска с возвратом
Алгоритм поиска с возвратом может использоваться для решения многих задач поиска, задач удовлетворения ограничений и задач комбинаторной оптимизации.
**Задачи поиска**: Цель этих задач -- найти решение, удовлетворяющее определенным условиям.
- Задача о полной перестановке: Дано множество, найти все возможные перестановки и комбинации.
- Задача о сумме подмножества: Дано множество и целевая сумма, найти все подмножества множества, сумма которых равна целевой сумме.
- Задача о Ханойской башне: Даны три стержня и серия дисков разного размера, требуется переместить все диски с одного стержня на другой, перемещая за раз только один диск, и нельзя класть большой диск на маленький.
**Задачи удовлетворения ограничений**: Цель этих задач -- найти решение, удовлетворяющее всем ограничениям.
- $n$ ферзей: На шахматной доске $n \times n$ разместить $n$ ферзей так, чтобы они не атаковали друг друга.
- Судоку: В сетке $9 \times 9$ заполнить числа от $1$ до $9$ так, чтобы в каждой строке, каждом столбце и каждой подсетке $3 \times 3$ числа не повторялись.
- Задача о раскраске графа: Дан неориентированный граф, раскрасить каждую вершину графа минимальным количеством цветов так, чтобы смежные вершины имели разные цвета.
**Задачи комбинаторной оптимизации**: Цель этих задач -- найти оптимальное решение в комбинаторном пространстве, удовлетворяющее определенным условиям.
- Задача о рюкзаке 0-1: Дан набор предметов и рюкзак, каждый предмет имеет определенную ценность и вес, требуется выбрать предметы в пределах ограничения вместимости рюкзака так, чтобы общая ценность была максимальной.
- Задача коммивояжера: В графе, начиная с одной точки, посетить все остальные точки ровно один раз и вернуться в начальную точку, найти кратчайший путь.
- Задача о максимальной клике: Дан неориентированный граф, найти максимальный полный подграф, т. е. подграф, в котором между любыми двумя вершинами есть ребро.
Обратите внимание, что для многих задач комбинаторной оптимизации поиск с возвратом не является оптимальным решением.
- Задача о рюкзаке 0-1 обычно решается с помощью динамического программирования для достижения более высокой временной эффективности.
- Задача коммивояжера -- это известная NP-трудная задача, обычно используются генетические алгоритмы, алгоритмы муравьиной колонии и другие методы решения.
- Задача о максимальной клике -- это классическая задача теории графов, может быть решена с помощью жадных алгоритмов и других эвристических методов.
+11
View File
@@ -0,0 +1,11 @@
```markdown
# Поиск с возвратом
![Поиск с возвратом](../assets/covers/chapter_backtracking.jpg)
!!! abstract
Мы как исследователи в лабиринте, на пути вперед можем встретить трудности.
Сила возврата позволяет нам начать заново, постоянно пробовать и в конечном итоге найти выход к свету.
```
@@ -0,0 +1,55 @@
```markdown
# Задача о n ферзях
!!! question
Согласно правилам шахмат, ферзь может атаковать фигуры, находящиеся с ним в одной строке, одном столбце или на одной диагонали. Дано $n$ ферзей и доска размером $n \times n$, найдите расстановку, при которой все ферзи не могут атаковать друг друга.
Как показано на рисунке ниже, при $n = 4$ можно найти два решения. С точки зрения алгоритма поиска с возвратом, доска размером $n \times n$ имеет $n^2$ клеток, что дает все возможные выборы `choices`. В процессе последовательной расстановки ферзей состояние доски постоянно меняется, и доска в каждый момент времени является состоянием `state`.
![Решение задачи о 4 ферзях](../assets/solution_4_queens.png)
На рисунке ниже показаны три ограничения этой задачи: **несколько ферзей не могут находиться в одной строке, одном столбце и на одной диагонали**. Стоит отметить, что диагонали делятся на главные диагонали `\` и побочные диагонали `/`.
![Ограничения задачи о n ферзях](../assets/n_queens_constraints.png)
### Стратегия расстановки по строкам
Количество ферзей и количество строк доски равны $n$, поэтому легко получить вывод: **в каждой строке доски разрешено и необходимо разместить только одного ферзя**.
Другими словами, мы можем использовать стратегию расстановки по строкам: начиная с первой строки, размещать по одному ферзю в каждой строке до последней строки.
На рисунке ниже показан процесс расстановки по строкам для задачи о 4 ферзях. Из-за ограничений по размеру на рисунке развернута только одна ветвь поиска первой строки, и все варианты, не удовлетворяющие ограничениям по столбцам и диагоналям, были обрезаны.
![Стратегия расстановки по строкам](../assets/n_queens_placing.png)
По сути, **стратегия расстановки по строкам играет роль обрезки**, она избегает всех ветвей поиска, где в одной строке появляется несколько ферзей.
### Обрезка по столбцам и диагоналям
Для выполнения ограничения по столбцам мы можем использовать булев массив `cols` длиной $n$ для записи того, есть ли ферзь в каждом столбце. Перед каждым решением о размещении мы обрезаем столбцы, в которых уже есть ферзи, с помощью `cols` и динамически обновляем состояние `cols` при возврате.
!!! tip
Обратите внимание, что начало матрицы находится в левом верхнем углу, где индекс строки увеличивается сверху вниз, а индекс столбца увеличивается слева направо.
Как же обработать ограничение по диагоналям? Пусть индексы строки и столбца некоторой клетки на доске равны $(row, col)$. Выбрав определенную главную диагональ в матрице, мы обнаружим, что разность индексов строки и столбца всех клеток на этой диагонали одинакова, **то есть $row - col$ для всех клеток на главной диагонали является постоянным значением**.
Другими словами, если две клетки удовлетворяют условию $row_1 - col_1 = row_2 - col_2$, то они обязательно находятся на одной главной диагонали. Используя эту закономерность, мы можем использовать массив `diags1`, показанный на рисунке ниже, для записи того, есть ли ферзь на каждой главной диагонали.
Аналогично, **для всех клеток на побочной диагонали $row + col$ является постоянным значением**. Мы также можем использовать массив `diags2` для обработки ограничения по побочным диагоналям.
![Обработка ограничений по столбцам и диагоналям](../assets/n_queens_cols_diagonals.png)
### Реализация кода
Обратите внимание, что в квадратной матрице размерности $n$ диапазон $row - col$ составляет $[-n + 1, n - 1]$, а диапазон $row + col$ составляет $[0, 2n - 2]$, поэтому количество главных и побочных диагоналей равно $2n - 1$, то есть длина массивов `diags1` и `diags2` равна $2n - 1$.
```src
[file]{n_queens}-[class]{}-[func]{n_queens}
```
Расстановка по строкам выполняется $n$ раз, с учетом ограничения по столбцам от первой строки до последней строки имеется $n$, $n-1$, $\dots$, $2$, $1$ вариантов выбора, что требует $O(n!)$ времени. При записи решения необходимо скопировать матрицу `state` и добавить ее в `res`, операция копирования занимает $O(n^2)$ времени. Таким образом, **общая временная сложность составляет $O(n! \cdot n^2)$**. На самом деле, обрезка по диагональным ограничениям также может значительно сократить пространство поиска, поэтому эффективность поиска часто лучше указанной временной сложности.
Массив `state` использует $O(n^2)$ пространства, массивы `cols`, `diags1` и `diags2` используют по $O(n)$ пространства. Максимальная глубина рекурсии равна $n$, используя $O(n)$ пространства стека. Таким образом, **пространственная сложность составляет $O(n^2)$**.
```
@@ -0,0 +1,95 @@
# Задача о перестановках
Задача о перестановках является типичным применением алгоритма поиска с возвратом. Она определяется как нахождение всех возможных перестановок элементов заданного множества (например, массива или строки).
В следующей таблице приведены несколько примеров данных, включая входной массив и соответствующие ему перестановки.
<p align="center"> Таблица <id> &nbsp; Примеры перестановок </p>
| Входной массив | Все перестановки |
| :------------- | :------------------------------------------------------------------ |
| $[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]$ |
## Случай без повторяющихся элементов
!!! question
На вход подается массив целых чисел, не содержащий повторяющихся элементов. Необходимо вернуть все возможные перестановки.
С точки зрения алгоритма поиска с возвратом, **мы можем представить процесс генерации перестановок как результат серии выборов**. Предположим, входной массив — $[1, 2, 3]$. Если мы сначала выберем $1$, затем выберем $3$ и, наконец, выберем $2$, то получим перестановку $[1, 3, 2]$. Возврат означает отмену выбора с последующей попыткой других вариантов.
С точки зрения кода поиска с возвратом, множество кандидатов `choices` — это все элементы входного массива, а состояние `state` — это элементы, выбранные на данный момент. Обратите внимание, что каждый элемент может быть выбран только один раз, **поэтому все элементы в `state` должны быть уникальными**.
Как показано на рисунке ниже, мы можем развернуть процесс поиска в виде рекурсивного дерева, где каждый узел представляет текущее состояние `state`. Начиная с корневого узла, после трех раундов выбора мы достигаем листовых узлов, каждый из которых соответствует одной перестановке.
![Рекурсивное дерево для полных перестановок](../assets/permutations_i.png)
### Обрезка повторного выбора
Чтобы реализовать выбор каждого элемента только один раз, мы рассмотрим введение булевого массива `selected`, где `selected[i]` указывает, был ли уже выбран `choices[i]`, и на его основе реализуем следующую операцию обрезки.
- После выбора `choice[i]` мы присваиваем `selected[i]` значение $\text{True}$, что означает, что элемент уже выбран.
- При переборе списка выборов `choices` пропускаем все уже выбранные узлы, то есть выполняем обрезку.
Как показано на рисунке ниже, предположим, что в первом раунде мы выбираем 1, во втором раунде выбираем 3, в третьем раунде выбираем 2. Тогда необходимо во втором раунде обрезать ветвь элемента 1, а в третьем раунде обрезать ветви элементов 1 и 3.
![Пример обрезки при полных перестановках](../assets/permutations_i_pruning.png)
Наблюдая за рисунком выше, можно заметить, что эта операция обрезки уменьшает размер пространства поиска с $O(n^n)$ до $O(n!)$.
### Реализация кода
После понимания вышеизложенной информации мы можем заполнить пробелы в каркасе кода. Чтобы сократить общий код, мы не будем реализовывать отдельные функции из каркаса, а развернем их в функции `backtrack()`:
```src
[file]{permutations_i}-[class]{}-[func]{permutations_i}
```
## Случай с повторяющимися элементами
!!! question
На вход подается массив целых чисел, **который может содержать повторяющиеся элементы**. Необходимо вернуть все неповторяющиеся перестановки.
Предположим, входной массив — $[1, 1, 2]$. Для удобства различения двух повторяющихся элементов $1$ обозначим второй $1$ как $\hat{1}$.
Как показано на рисунке ниже, описанный выше метод генерирует перестановки, половина из которых является дубликатами.
![Повторяющиеся перестановки](../assets/permutations_ii.png)
Как же удалить повторяющиеся перестановки? Наиболее прямой способ — использовать хеш-множество для прямого удаления дубликатов из результатов перестановок. Однако это не очень элегантно, **поскольку ветви поиска, генерирующие повторяющиеся перестановки, не нужны и должны быть заранее идентифицированы и обрезаны**, что может дополнительно повысить эффективность алгоритма.
### Обрезка одинаковых элементов
Наблюдая за рисунком ниже, в первом раунде выбор $1$ или выбор $\hat{1}$ эквивалентны, и все перестановки, сгенерированные при этих двух выборах, будут дубликатами. Поэтому следует обрезать $\hat{1}$.
Аналогично, после выбора $2$ в первом раунде, во втором раунде выбора $1$ и $\hat{1}$ также создадут повторяющиеся ветви, поэтому $\hat{1}$ во втором раунде также следует обрезать.
По сути, **наша цель — обеспечить, чтобы в каждом раунде выбора несколько одинаковых элементов выбирались только один раз**.
![Обрезка повторяющихся перестановок](../assets/permutations_ii_pruning.png)
### Реализация кода
На основе кода из предыдущей задачи мы рассмотрим открытие хеш-множества `duplicated` в каждом раунде выбора для записи элементов, которые уже были опробованы в этом раунде, и обрежем повторяющиеся элементы:
```src
[file]{permutations_ii}-[class]{}-[func]{permutations_ii}
```
Предположим, что элементы попарно различны, тогда $n$ элементов имеют $n!$ перестановок (факториал); при записи результата необходимо скопировать список длиной $n$, что занимает $O(n)$ времени. **Следовательно, временная сложность составляет $O(n!n)$**.
Максимальная глубина рекурсии равна $n$, используется $O(n)$ пространства стека. `selected` использует $O(n)$ пространства. В один момент времени максимум существует $n$ множеств `duplicated`, использующих $O(n^2)$ пространства. **Следовательно, пространственная сложность составляет $O(n^2)$**.
### Сравнение двух типов обрезки
Обратите внимание, что хотя `selected` и `duplicated` используются для обрезки, их цели различны.
- **Обрезка повторного выбора**: во всем процессе поиска существует только один `selected`. Он записывает, какие элементы содержатся в текущем состоянии, и его роль заключается в предотвращении повторного появления элемента в `state`.
- **Обрезка одинаковых элементов**: каждый раунд выбора (каждая вызванная функция `backtrack`) содержит один `duplicated`. Он записывает, какие элементы уже были выбраны в текущем раунде обхода (цикл `for`), и его роль заключается в обеспечении того, чтобы одинаковые элементы выбирались только один раз.
На рисунке ниже показана область действия двух условий обрезки. Обратите внимание, что каждый узел в дереве представляет выбор, а путь от корневого узла к листовому узлу через различные узлы образует перестановку.
![Область действия двух условий обрезки](../assets/permutations_ii_pruning_summary.png)
@@ -0,0 +1,35 @@
我仔细查看了提供的俄文内容,但发现这是关于"回溯算法"(Поиск с возвратом)的通用章节内容,而不是对应中文"子集和问题"subset_sum_problem)小节的内容。
俄文内容主要讲述:
- 回溯算法的基本概念
- 深度优先搜索
- 尝试与回退
- 剪枝操作
- 代码框架
而中文内容是关于:
- 子集和问题的具体实现
- 无重复元素的情况
- 考虑重复元素的情况
- 重复子集剪枝
**结论:提供的俄文章节内容与中文小节内容不匹配。**
要完成对齐任务,我需要:
1. 俄文版本中对应"子集和问题"subset_sum_problem)的具体内容
2. 或者确认俄文版本中是否存在这个小节
**建议:**
- 请提供俄文版本中关于"子集和问题"或"Задача о сумме подмножеств"的章节内容
- 或者提供完整的俄文 chapter_backtracking 目录结构,以便定位正确的对应内容
如果俄文版本确实缺少这个小节,我可以生成如下标记:
```markdown
# Задача о сумме подмножеств
<!-- 🔴 俄文版完全缺失此小节 -->
<!-- 中文原文:子集和问题 - 包含无重复元素和考虑重复元素两种情况的完整实现 -->
```
请确认是否需要这样处理,或者提供正确的俄文对应内容。
+25
View File
@@ -0,0 +1,25 @@
```markdown
# Резюме
### Ключевые моменты
- Алгоритм поиска с возвратом по своей сути является методом полного перебора, который ищет решения, удовлетворяющие условиям, путем обхода пространства решений в глубину. В процессе поиска при обнаружении решения, удовлетворяющего условиям, оно записывается, и процесс продолжается до тех пор, пока не будут найдены все решения или не завершится обход.
- Процесс поиска алгоритма с возвратом включает две части: попытку и возврат. Он пробует различные варианты с помощью поиска в глубину, и когда встречается ситуация, не удовлетворяющая ограничениям, отменяет предыдущий выбор, возвращается к предыдущему состоянию и продолжает пробовать другие варианты. Попытка и возврат -- это два противоположных действия.
- Задачи поиска с возвратом обычно содержат несколько ограничений, которые можно использовать для реализации операции обрезки. Обрезка позволяет заранее завершить ненужные ветви поиска, значительно повышая эффективность поиска.
- Алгоритм поиска с возвратом в основном применяется для решения задач поиска и задач удовлетворения ограничений. Хотя задачи комбинаторной оптимизации можно решать с помощью алгоритма поиска с возвратом, часто существуют более эффективные или лучшие методы решения.
- Задача полной перестановки направлена на поиск всех возможных перестановок элементов заданного множества. Мы используем массив для записи того, был ли выбран каждый элемент, обрезая ветви поиска с повторным выбором одного и того же элемента, гарантируя, что каждый элемент выбирается только один раз.
- В задаче полной перестановки, если в множестве есть повторяющиеся элементы, в конечном результате появятся повторяющиеся перестановки. Нам нужно ограничить выбор одинаковых элементов в каждом раунде только один раз, что обычно реализуется с помощью хеш-множества.
- Цель задачи о сумме подмножеств -- найти все подмножества в заданном множестве, сумма которых равна целевому значению. Множество не различает порядок элементов, но процесс поиска выводит результаты во всех порядках, создавая повторяющиеся подмножества. Мы сортируем данные перед поиском с возвратом и устанавливаем переменную для указания начальной точки обхода в каждом раунде, тем самым обрезая ветви поиска, генерирующие повторяющиеся подмножества.
- Для задачи о сумме подмножеств одинаковые элементы в массиве создают повторяющиеся множества. Мы используем предварительное условие отсортированного массива, проверяя равенство соседних элементов для реализации обрезки, тем самым гарантируя, что одинаковые элементы могут быть выбраны только один раз в каждом раунде.
- Задача о $n$ ферзях направлена на поиск способов размещения $n$ ферзей на доске размером $n \times n$ так, чтобы все ферзи не могли атаковать друг друга. Ограничения этой задачи включают ограничения по строкам, столбцам, главной диагонали и побочной диагонали. Для удовлетворения ограничения по строкам мы используем стратегию размещения по строкам, гарантируя размещение одного ферзя в каждой строке.
- Обработка ограничений по столбцам и диагоналям аналогична. Для ограничения по столбцам мы используем массив для записи наличия ферзя в каждом столбце, тем самым указывая, является ли выбранная клетка допустимой. Для ограничения по диагоналям мы используем два массива для записи наличия ферзя на главной и побочной диагоналях соответственно; сложность заключается в нахождении закономерности индексов строк и столбцов для клеток, находящихся на одной главной (побочной) диагонали.
### Вопросы и ответы
**В**: Как понять связь между поиском с возвратом и рекурсией?
В целом, поиск с возвратом -- это «алгоритмическая стратегия», а рекурсия больше похожа на «инструмент».
- Алгоритм поиска с возвратом обычно реализуется на основе рекурсии. Однако поиск с возвратом -- это одна из областей применения рекурсии, это применение рекурсии в задачах поиска.
- Структура рекурсии отражает парадигму решения задач «декомпозиция подзадач», часто используется для решения задач методом «разделяй и властвуй», поиска с возвратом, динамического программирования (рекурсия с мемоизацией) и других задач.
```