mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-16 08:56:05 +00:00
First version.
This commit is contained in:
@@ -0,0 +1,503 @@
|
||||
# Поиск с возвратом
|
||||
|
||||
<u>Алгоритм поиска с возвратом (backtracking algorithm)</u> -- это метод решения задач путем перебора. Его основная идея заключается в том, чтобы, начиная с начального состояния, осуществлять грубый поиск всех возможных решений, фиксируя правильное найденное решение. Процесс поиска продолжается до тех пор, пока не будет найдено решение или не будут исчерпаны все возможные варианты.
|
||||
|
||||
Алгоритмы поиска с возвратом обычно используют поиск в глубину для обхода пространства решений. В разделе «Двоичные деревья» упоминалось, что прямой, симметричный и обратный обходы относятся к поиску в глубину. Далее, используя прямой обход, мы реализуем задачу поиска с возвратом, чтобы постепенно понять принцип работы этого алгоритма.
|
||||
|
||||
!!! question "Пример 1"
|
||||
|
||||
Дано двоичное дерево, найти и записать все узлы со значением $7$, вернуть список узлов.
|
||||
|
||||
Для решения этой задачи мы выполняем предварительный обход дерева и проверяем, равно ли значение текущего узла $7$. Если равно, то добавляем значение этого узла в список результатов `res`. Процесс представлен на следующем рисунке и в коде ниже:
|
||||
|
||||
```src
|
||||
[file]{preorder_traversal_i_compact}-[class]{}-[func]{pre_order}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Попытка и возврат
|
||||
|
||||
**Алгоритм называется поиском с возвратом, потому что при поиске в пространстве решений он использует стратегию попытки и возврата**. Когда алгоритм сталкивается с состоянием, в котором невозможно продолжать или невозможно получить удовлетворительное решение, он отменяет предыдущий выбор, возвращается к предыдущему состоянию и пробует другие возможные варианты.
|
||||
|
||||
В примере 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>"
|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
|
||||
=== "<3>"
|
||||

|
||||
|
||||
=== "<4>"
|
||||

|
||||
|
||||
=== "<5>"
|
||||

|
||||
|
||||
=== "<6>"
|
||||

|
||||
|
||||
=== "<7>"
|
||||

|
||||
|
||||
=== "<8>"
|
||||

|
||||
|
||||
=== "<9>"
|
||||

|
||||
|
||||
=== "<10>"
|
||||

|
||||
|
||||
=== "<11>"
|
||||

|
||||
|
||||
## Обрезка
|
||||
|
||||
Сложные задачи поиска с возвратом обычно содержат одно или несколько ограничений, **которые можно использовать для обрезки**.
|
||||
|
||||
!!! question "Пример 3"
|
||||
|
||||
В двоичном дереве найти все узлы со значением $7$, вернуть пути от корневого узла до этих узлов, **при этом путь не должен содержать узлы со значением $3$**.
|
||||
|
||||
Для выполнения данного условия **требуется добавить операцию обрезки**: в процессе поиска, если встречается узел со значением $3$, следует немедленно вернуться, не продолжая поиск. Код реализации представлен ниже:
|
||||
|
||||
```src
|
||||
[file]{preorder_traversal_iii_compact}-[class]{}-[func]{pre_order}
|
||||
```
|
||||
|
||||
Обрезка является очень наглядным термином. В процессе поиска, как показано на следующем рисунке, **мы обрезаем ветви поиска, не удовлетворяющие заданным условиям**, и избегаем множества бессмысленных попыток, тем самым повышая эффективность поиска.
|
||||
|
||||

|
||||
|
||||
## Каркас кода
|
||||
|
||||
Далее мы попытаемся сформировать основной каркас операций «попытка, возврат, обрезка» для повышения универсальности кода.
|
||||
|
||||
В следующем каркасе кода `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`.
|
||||
|
||||

|
||||
|
||||
По сравнению с реализацией на основе предварительного обхода, реализация на основе каркаса поиска с возвратом выглядит более громоздкой, но обладает большей универсальностью. На самом деле **многие задачи поиска с возвратом можно решить в рамках этого каркаса**. Необходимо лишь определить `state` и `choices` в соответствии с конкретной задачей и реализовать методы каркаса.
|
||||
|
||||
## Основные термины
|
||||
|
||||
Для более четкого понимания алгоритмических задач мы систематизируем значения часто используемых терминов обратного поиска и приведем соответствующие примеры для задачи 3, как показано в следующей таблице.
|
||||
|
||||
<p align="center"> Таблица <id> Основные термины обратного поиска </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-трудная задача, обычно используются генетические алгоритмы, алгоритмы муравьиной колонии и другие методы решения.
|
||||
- Задача о максимальной клике -- это классическая задача теории графов, может быть решена с помощью жадных алгоритмов и других эвристических методов.
|
||||
@@ -0,0 +1,11 @@
|
||||
```markdown
|
||||
# Поиск с возвратом
|
||||
|
||||

|
||||
|
||||
!!! abstract
|
||||
|
||||
Мы как исследователи в лабиринте, на пути вперед можем встретить трудности.
|
||||
|
||||
Сила возврата позволяет нам начать заново, постоянно пробовать и в конечном итоге найти выход к свету.
|
||||
```
|
||||
@@ -0,0 +1,55 @@
|
||||
```markdown
|
||||
# Задача о n ферзях
|
||||
|
||||
!!! question
|
||||
|
||||
Согласно правилам шахмат, ферзь может атаковать фигуры, находящиеся с ним в одной строке, одном столбце или на одной диагонали. Дано $n$ ферзей и доска размером $n \times n$, найдите расстановку, при которой все ферзи не могут атаковать друг друга.
|
||||
|
||||
Как показано на рисунке ниже, при $n = 4$ можно найти два решения. С точки зрения алгоритма поиска с возвратом, доска размером $n \times n$ имеет $n^2$ клеток, что дает все возможные выборы `choices`. В процессе последовательной расстановки ферзей состояние доски постоянно меняется, и доска в каждый момент времени является состоянием `state`.
|
||||
|
||||

|
||||
|
||||
На рисунке ниже показаны три ограничения этой задачи: **несколько ферзей не могут находиться в одной строке, одном столбце и на одной диагонали**. Стоит отметить, что диагонали делятся на главные диагонали `\` и побочные диагонали `/`.
|
||||
|
||||

|
||||
|
||||
### Стратегия расстановки по строкам
|
||||
|
||||
Количество ферзей и количество строк доски равны $n$, поэтому легко получить вывод: **в каждой строке доски разрешено и необходимо разместить только одного ферзя**.
|
||||
|
||||
Другими словами, мы можем использовать стратегию расстановки по строкам: начиная с первой строки, размещать по одному ферзю в каждой строке до последней строки.
|
||||
|
||||
На рисунке ниже показан процесс расстановки по строкам для задачи о 4 ферзях. Из-за ограничений по размеру на рисунке развернута только одна ветвь поиска первой строки, и все варианты, не удовлетворяющие ограничениям по столбцам и диагоналям, были обрезаны.
|
||||
|
||||

|
||||
|
||||
По сути, **стратегия расстановки по строкам играет роль обрезки**, она избегает всех ветвей поиска, где в одной строке появляется несколько ферзей.
|
||||
|
||||
### Обрезка по столбцам и диагоналям
|
||||
|
||||
Для выполнения ограничения по столбцам мы можем использовать булев массив `cols` длиной $n$ для записи того, есть ли ферзь в каждом столбце. Перед каждым решением о размещении мы обрезаем столбцы, в которых уже есть ферзи, с помощью `cols` и динамически обновляем состояние `cols` при возврате.
|
||||
|
||||
!!! tip
|
||||
|
||||
Обратите внимание, что начало матрицы находится в левом верхнем углу, где индекс строки увеличивается сверху вниз, а индекс столбца увеличивается слева направо.
|
||||
|
||||
Как же обработать ограничение по диагоналям? Пусть индексы строки и столбца некоторой клетки на доске равны $(row, col)$. Выбрав определенную главную диагональ в матрице, мы обнаружим, что разность индексов строки и столбца всех клеток на этой диагонали одинакова, **то есть $row - col$ для всех клеток на главной диагонали является постоянным значением**.
|
||||
|
||||
Другими словами, если две клетки удовлетворяют условию $row_1 - col_1 = row_2 - col_2$, то они обязательно находятся на одной главной диагонали. Используя эту закономерность, мы можем использовать массив `diags1`, показанный на рисунке ниже, для записи того, есть ли ферзь на каждой главной диагонали.
|
||||
|
||||
Аналогично, **для всех клеток на побочной диагонали $row + col$ является постоянным значением**. Мы также можем использовать массив `diags2` для обработки ограничения по побочным диагоналям.
|
||||
|
||||

|
||||
|
||||
### Реализация кода
|
||||
|
||||
Обратите внимание, что в квадратной матрице размерности $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> Примеры перестановок </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`. Начиная с корневого узла, после трех раундов выбора мы достигаем листовых узлов, каждый из которых соответствует одной перестановке.
|
||||
|
||||

|
||||
|
||||
### Обрезка повторного выбора
|
||||
|
||||
Чтобы реализовать выбор каждого элемента только один раз, мы рассмотрим введение булевого массива `selected`, где `selected[i]` указывает, был ли уже выбран `choices[i]`, и на его основе реализуем следующую операцию обрезки.
|
||||
|
||||
- После выбора `choice[i]` мы присваиваем `selected[i]` значение $\text{True}$, что означает, что элемент уже выбран.
|
||||
- При переборе списка выборов `choices` пропускаем все уже выбранные узлы, то есть выполняем обрезку.
|
||||
|
||||
Как показано на рисунке ниже, предположим, что в первом раунде мы выбираем 1, во втором раунде выбираем 3, в третьем раунде выбираем 2. Тогда необходимо во втором раунде обрезать ветвь элемента 1, а в третьем раунде обрезать ветви элементов 1 и 3.
|
||||
|
||||

|
||||
|
||||
Наблюдая за рисунком выше, можно заметить, что эта операция обрезки уменьшает размер пространства поиска с $O(n^n)$ до $O(n!)$.
|
||||
|
||||
### Реализация кода
|
||||
|
||||
После понимания вышеизложенной информации мы можем заполнить пробелы в каркасе кода. Чтобы сократить общий код, мы не будем реализовывать отдельные функции из каркаса, а развернем их в функции `backtrack()`:
|
||||
|
||||
```src
|
||||
[file]{permutations_i}-[class]{}-[func]{permutations_i}
|
||||
```
|
||||
|
||||
## Случай с повторяющимися элементами
|
||||
|
||||
!!! question
|
||||
|
||||
На вход подается массив целых чисел, **который может содержать повторяющиеся элементы**. Необходимо вернуть все неповторяющиеся перестановки.
|
||||
|
||||
Предположим, входной массив — $[1, 1, 2]$. Для удобства различения двух повторяющихся элементов $1$ обозначим второй $1$ как $\hat{1}$.
|
||||
|
||||
Как показано на рисунке ниже, описанный выше метод генерирует перестановки, половина из которых является дубликатами.
|
||||
|
||||

|
||||
|
||||
Как же удалить повторяющиеся перестановки? Наиболее прямой способ — использовать хеш-множество для прямого удаления дубликатов из результатов перестановок. Однако это не очень элегантно, **поскольку ветви поиска, генерирующие повторяющиеся перестановки, не нужны и должны быть заранее идентифицированы и обрезаны**, что может дополнительно повысить эффективность алгоритма.
|
||||
|
||||
### Обрезка одинаковых элементов
|
||||
|
||||
Наблюдая за рисунком ниже, в первом раунде выбор $1$ или выбор $\hat{1}$ эквивалентны, и все перестановки, сгенерированные при этих двух выборах, будут дубликатами. Поэтому следует обрезать $\hat{1}$.
|
||||
|
||||
Аналогично, после выбора $2$ в первом раунде, во втором раунде выбора $1$ и $\hat{1}$ также создадут повторяющиеся ветви, поэтому $\hat{1}$ во втором раунде также следует обрезать.
|
||||
|
||||
По сути, **наша цель — обеспечить, чтобы в каждом раунде выбора несколько одинаковых элементов выбирались только один раз**.
|
||||
|
||||

|
||||
|
||||
### Реализация кода
|
||||
|
||||
На основе кода из предыдущей задачи мы рассмотрим открытие хеш-множества `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`), и его роль заключается в обеспечении того, чтобы одинаковые элементы выбирались только один раз.
|
||||
|
||||
На рисунке ниже показана область действия двух условий обрезки. Обратите внимание, что каждый узел в дереве представляет выбор, а путь от корневого узла к листовому узлу через различные узлы образует перестановку.
|
||||
|
||||

|
||||
@@ -0,0 +1,35 @@
|
||||
我仔细查看了提供的俄文内容,但发现这是关于"回溯算法"(Поиск с возвратом)的通用章节内容,而不是对应中文"子集和问题"(subset_sum_problem)小节的内容。
|
||||
|
||||
俄文内容主要讲述:
|
||||
- 回溯算法的基本概念
|
||||
- 深度优先搜索
|
||||
- 尝试与回退
|
||||
- 剪枝操作
|
||||
- 代码框架
|
||||
|
||||
而中文内容是关于:
|
||||
- 子集和问题的具体实现
|
||||
- 无重复元素的情况
|
||||
- 考虑重复元素的情况
|
||||
- 重复子集剪枝
|
||||
|
||||
**结论:提供的俄文章节内容与中文小节内容不匹配。**
|
||||
|
||||
要完成对齐任务,我需要:
|
||||
1. 俄文版本中对应"子集和问题"(subset_sum_problem)的具体内容
|
||||
2. 或者确认俄文版本中是否存在这个小节
|
||||
|
||||
**建议:**
|
||||
- 请提供俄文版本中关于"子集和问题"或"Задача о сумме подмножеств"的章节内容
|
||||
- 或者提供完整的俄文 chapter_backtracking 目录结构,以便定位正确的对应内容
|
||||
|
||||
如果俄文版本确实缺少这个小节,我可以生成如下标记:
|
||||
|
||||
```markdown
|
||||
# Задача о сумме подмножеств
|
||||
|
||||
<!-- 🔴 俄文版完全缺失此小节 -->
|
||||
<!-- 中文原文:子集和问题 - 包含无重复元素和考虑重复元素两种情况的完整实现 -->
|
||||
```
|
||||
|
||||
请确认是否需要这样处理,或者提供正确的俄文对应内容。
|
||||
@@ -0,0 +1,25 @@
|
||||
```markdown
|
||||
# Резюме
|
||||
|
||||
### Ключевые моменты
|
||||
|
||||
- Алгоритм поиска с возвратом по своей сути является методом полного перебора, который ищет решения, удовлетворяющие условиям, путем обхода пространства решений в глубину. В процессе поиска при обнаружении решения, удовлетворяющего условиям, оно записывается, и процесс продолжается до тех пор, пока не будут найдены все решения или не завершится обход.
|
||||
- Процесс поиска алгоритма с возвратом включает две части: попытку и возврат. Он пробует различные варианты с помощью поиска в глубину, и когда встречается ситуация, не удовлетворяющая ограничениям, отменяет предыдущий выбор, возвращается к предыдущему состоянию и продолжает пробовать другие варианты. Попытка и возврат -- это два противоположных действия.
|
||||
- Задачи поиска с возвратом обычно содержат несколько ограничений, которые можно использовать для реализации операции обрезки. Обрезка позволяет заранее завершить ненужные ветви поиска, значительно повышая эффективность поиска.
|
||||
- Алгоритм поиска с возвратом в основном применяется для решения задач поиска и задач удовлетворения ограничений. Хотя задачи комбинаторной оптимизации можно решать с помощью алгоритма поиска с возвратом, часто существуют более эффективные или лучшие методы решения.
|
||||
- Задача полной перестановки направлена на поиск всех возможных перестановок элементов заданного множества. Мы используем массив для записи того, был ли выбран каждый элемент, обрезая ветви поиска с повторным выбором одного и того же элемента, гарантируя, что каждый элемент выбирается только один раз.
|
||||
- В задаче полной перестановки, если в множестве есть повторяющиеся элементы, в конечном результате появятся повторяющиеся перестановки. Нам нужно ограничить выбор одинаковых элементов в каждом раунде только один раз, что обычно реализуется с помощью хеш-множества.
|
||||
- Цель задачи о сумме подмножеств -- найти все подмножества в заданном множестве, сумма которых равна целевому значению. Множество не различает порядок элементов, но процесс поиска выводит результаты во всех порядках, создавая повторяющиеся подмножества. Мы сортируем данные перед поиском с возвратом и устанавливаем переменную для указания начальной точки обхода в каждом раунде, тем самым обрезая ветви поиска, генерирующие повторяющиеся подмножества.
|
||||
- Для задачи о сумме подмножеств одинаковые элементы в массиве создают повторяющиеся множества. Мы используем предварительное условие отсортированного массива, проверяя равенство соседних элементов для реализации обрезки, тем самым гарантируя, что одинаковые элементы могут быть выбраны только один раз в каждом раунде.
|
||||
- Задача о $n$ ферзях направлена на поиск способов размещения $n$ ферзей на доске размером $n \times n$ так, чтобы все ферзи не могли атаковать друг друга. Ограничения этой задачи включают ограничения по строкам, столбцам, главной диагонали и побочной диагонали. Для удовлетворения ограничения по строкам мы используем стратегию размещения по строкам, гарантируя размещение одного ферзя в каждой строке.
|
||||
- Обработка ограничений по столбцам и диагоналям аналогична. Для ограничения по столбцам мы используем массив для записи наличия ферзя в каждом столбце, тем самым указывая, является ли выбранная клетка допустимой. Для ограничения по диагоналям мы используем два массива для записи наличия ферзя на главной и побочной диагоналях соответственно; сложность заключается в нахождении закономерности индексов строк и столбцов для клеток, находящихся на одной главной (побочной) диагонали.
|
||||
|
||||
### Вопросы и ответы
|
||||
|
||||
**В**: Как понять связь между поиском с возвратом и рекурсией?
|
||||
|
||||
В целом, поиск с возвратом -- это «алгоритмическая стратегия», а рекурсия больше похожа на «инструмент».
|
||||
|
||||
- Алгоритм поиска с возвратом обычно реализуется на основе рекурсии. Однако поиск с возвратом -- это одна из областей применения рекурсии, это применение рекурсии в задачах поиска.
|
||||
- Структура рекурсии отражает парадигму решения задач «декомпозиция подзадач», часто используется для решения задач методом «разделяй и властвуй», поиска с возвратом, динамического программирования (рекурсия с мемоизацией) и других задач.
|
||||
```
|
||||
Reference in New Issue
Block a user