Add ru version (#1865)
* Add Russian docs site baseline * Add Russian localized codebase * Polish Russian code wording * Update ru code translation. * Update code translation and chapter covers. * Fix pythontutor extraction. * Add README and landing page. * placeholder of profiles * Use figures of English version * Remove chapter paperbook
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,503 @@
|
||||
# Алгоритм поиска с возвратом
|
||||
|
||||
<u>Алгоритм поиска с возвратом (backtracking algorithm)</u> - это метод решения задач путем полного перебора. Его основная идея состоит в том, чтобы, начиная с некоторого исходного состояния, грубо перебрать все возможные решения, записывать корректные решения и продолжать поиск до тех пор, пока решение не будет найдено или пока не будут исчерпаны все возможные варианты.
|
||||
|
||||
Обычно алгоритмы поиска с возвратом используют "поиск в глубину" для обхода пространства решений. В главе "Бинарные деревья" мы уже упоминали, что прямой, симметричный и обратный обходы относятся к поиску в глубину. Теперь мы на основе прямого обхода построим задачу backtracking и постепенно разберем принцип работы этого алгоритма.
|
||||
|
||||
!!! question "Пример 1"
|
||||
|
||||
Дано двоичное дерево. Найдите и запишите все узлы со значением $7$ ; верните список этих узлов.
|
||||
|
||||
Для этой задачи мы выполняем прямой обход дерева и проверяем, равно ли значение текущего узла $7$ ; если да, то добавляем значение этого узла в список результатов `res` . Соответствующий процесс показан на рисунке ниже и в коде:
|
||||
|
||||
```src
|
||||
[file]{preorder_traversal_i_compact}-[class]{}-[func]{pre_order}
|
||||
```
|
||||
|
||||

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

|
||||
|
||||
## Обрезка
|
||||
|
||||
Сложные задачи backtracking обычно содержат одно или несколько ограничений, **которые часто можно использовать для "обрезки"**.
|
||||
|
||||
!!! 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` сохраняется и когда он удаляется.
|
||||
|
||||

|
||||
|
||||
По сравнению с реализацией на основе прямого обхода, версия на основе общего каркаса backtracking выглядит более громоздкой, но при этом обладает лучшей универсальностью. На практике **многие задачи backtracking можно решать в рамках этого каркаса**. Для этого нужно лишь определить `state` и `choices` под конкретную задачу и реализовать соответствующие методы каркаса.
|
||||
|
||||
## Часто используемые термины
|
||||
|
||||
Чтобы яснее анализировать алгоритмические задачи, подытожим значения часто используемых терминов backtracking и сопоставим их с примером 3, как показано в таблице ниже.
|
||||
|
||||
<p align="center"> Таблица <id> Часто используемые термины алгоритма backtracking </p>
|
||||
|
||||
| Термин | Определение | Пример 3 |
|
||||
| ------------------------ | -------------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
||||
| Решение (solution) | Решение - это ответ, удовлетворяющий условиям задачи; решений может быть одно или несколько | Все пути от корня до узла $7$ , удовлетворяющие ограничениям |
|
||||
| Ограничение (constraint) | Ограничение определяет допустимость решения и обычно используется для обрезки | Путь не содержит узлы со значением $3$ |
|
||||
| Состояние (state) | Состояние описывает ситуацию задачи в некоторый момент времени, включая уже сделанные выборы | Текущий путь посещенных узлов, то есть список узлов `path` |
|
||||
| Попытка (attempt) | Попытка - это исследование пространства решений на основе доступных выборов, включая выбор, обновление состояния и проверку, является ли состояние решением | Рекурсивный переход к левому или правому потомку, добавление узла в `path` и проверка, равно ли значение узла $7$ |
|
||||
| Откат (backtracking) | Откат означает отмену предыдущих выборов и возврат к более раннему состоянию при встрече состояния, не удовлетворяющего ограничениям | Завершение поиска при проходе через лист, окончании посещения узла или встрече узла со значением $3$ , то есть возврат из функции |
|
||||
| Обрезка (pruning) | Обрезка - это способ избегать бессмысленных путей поиска на основе свойств задачи и ее ограничений, повышающий эффективность | При встрече узла со значением $3$ поиск по этой ветви прекращается |
|
||||
|
||||
!!! tip
|
||||
|
||||
Такие понятия, как задача, решение и состояние, являются общими и встречаются не только в backtracking, но и в divide and conquer, динамическом программировании, жадных алгоритмах и других темах.
|
||||
|
||||
## Преимущества и ограничения
|
||||
|
||||
Алгоритм поиска с возвратом по своей сути является алгоритмом поиска в глубину, который перебирает все возможные решения, пока не найдет удовлетворяющее условиям. Преимущество этого подхода в том, что он позволяет находить все возможные решения и при разумной обрезке может работать весьма эффективно.
|
||||
|
||||
Однако при работе с большими или сложными задачами **эффективность backtracking может оказаться неприемлемой**.
|
||||
|
||||
- **Время**: backtracking обычно требует обхода всех возможных состояний пространства состояний, и его временная сложность может достигать экспоненциального или факториального порядка.
|
||||
- **Память**: при рекурсивных вызовах нужно хранить текущее состояние (например, путь, вспомогательные переменные для обрезки и т.д.), поэтому при большой глубине рекурсии потребность в памяти может стать значительной.
|
||||
|
||||
Тем не менее **backtracking по-прежнему остается лучшим решением для некоторых поисковых задач и задач удовлетворения ограничений**. В таких задачах заранее невозможно предсказать, какие выборы приведут к эффективному решению, поэтому приходится перебирать все возможные варианты. В этой ситуации **ключевым становится вопрос оптимизации эффективности** , и для этого обычно используют две стратегии.
|
||||
|
||||
- **Обрезка**: избегать поиска по тем путям, которые заведомо не приведут к решению, тем самым экономя время и память.
|
||||
- **Эвристический поиск**: вводить во время поиска дополнительные стратегии или оценки, чтобы в первую очередь исследовать пути, наиболее вероятно ведущие к эффективному решению.
|
||||
|
||||
## Типичные задачи backtracking
|
||||
|
||||
Алгоритм поиска с возвратом можно использовать для решения множества поисковых задач, задач удовлетворения ограничений и задач комбинаторной оптимизации.
|
||||
|
||||
**Поисковые задачи**: целью таких задач является поиск решений, удовлетворяющих определенным условиям.
|
||||
|
||||
- Задача о перестановках: дано множество, требуется найти все возможные перестановки его элементов.
|
||||
- Задача о сумме подмножеств: даны множество и целевая сумма; нужно найти все подмножества, сумма элементов которых равна целевой.
|
||||
- Задача о Ханойской башне: даны три стержня и набор дисков разного размера; требуется перенести все диски с одного стержня на другой, перемещая за раз только один диск и не помещая больший диск на меньший.
|
||||
|
||||
**Задачи удовлетворения ограничений**: целью таких задач является поиск решений, удовлетворяющих всем ограничениям.
|
||||
|
||||
- Задача о $n$ ферзях: разместить $n$ ферзей на шахматной доске размера $n \times n$ так, чтобы они не атаковали друг друга.
|
||||
- Судоку: заполнить сетку $9 \times 9$ числами от $1$ до $9$ так, чтобы в каждой строке, каждом столбце и каждом блоке $3 \times 3$ числа не повторялись.
|
||||
- Задача раскраски графа: дан неориентированный граф; требуется раскрасить его вершины минимальным числом цветов так, чтобы соседние вершины имели разные цвета.
|
||||
|
||||
**Задачи комбинаторной оптимизации**: целью таких задач является поиск оптимального решения в некотором комбинаторном пространстве при заданных ограничениях.
|
||||
|
||||
- Задача о рюкзаке 0-1: даны набор предметов и рюкзак; у каждого предмета есть ценность и вес, и нужно выбрать предметы так, чтобы при ограниченной вместимости рюкзака суммарная ценность была максимальной.
|
||||
- Задача коммивояжера: начиная из некоторой вершины графа, требуется посетить все остальные вершины ровно по одному разу и вернуться в исходную вершину, найдя при этом кратчайший путь.
|
||||
- Задача о максимальной клике: дан неориентированный граф; требуется найти в нем максимальный полный подграф, то есть подграф, в котором любая пара вершин соединена ребром.
|
||||
|
||||
Обратите внимание: для многих задач комбинаторной оптимизации backtracking не является оптимальным способом решения.
|
||||
|
||||
- Задача о рюкзаке 0-1 обычно решается с помощью динамического программирования, что дает более высокую временную эффективность.
|
||||
- Задача коммивояжера является известной NP-Hard задачей; для ее решения часто используют генетические алгоритмы, муравьиные алгоритмы и другие методы.
|
||||
- Задача о максимальной клике является классической задачей теории графов и может решаться жадными и другими эвристическими алгоритмами.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Поиск с возвратом
|
||||
|
||||

|
||||
|
||||
!!! abstract
|
||||
|
||||
Мы словно исследователи в лабиринте: на пути вперед могут встречаться тупики и трудности.
|
||||
|
||||
Сила возврата позволяет нам начать заново, пробовать снова и снова и в конце концов найти выход к свету.
|
||||
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
@@ -0,0 +1,53 @@
|
||||
# Задача о n ферзях
|
||||
|
||||
!!! question
|
||||
|
||||
Согласно правилам шахмат ферзь может атаковать фигуры, находящиеся с ним на одной строке, в одном столбце или на одной диагонали. Даны $n$ ферзей и шахматная доска размера $n \times n$ ; требуется найти такие расстановки, при которых ни одна пара ферзей не может атаковать друг друга.
|
||||
|
||||
Как показано на рисунке ниже, при $n = 4$ существует два решения. С точки зрения backtracking доска размера $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)$** .
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 18 KiB |
@@ -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
|
||||
|
||||
Дан массив целых чисел, в котором нет повторяющихся элементов. Верните все возможные перестановки.
|
||||
|
||||
С точки зрения backtracking **процесс построения перестановок можно представить как результат последовательности выборов**. Пусть входной массив равен $[1, 2, 3]$ ; если мы сначала выберем $1$ , затем $3$ , а потом $2$ , то получим перестановку $[1, 3, 2]$ . Откат означает отмену одного из выборов с последующей попыткой других вариантов.
|
||||
|
||||
С точки зрения кода backtracking множество кандидатов `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!)$ .
|
||||
|
||||
### Реализация кода
|
||||
|
||||
После прояснения всей логики можно просто "заполнить пропуски" в шаблоне backtracking. Чтобы сократить общий объем кода, мы не будем отдельно реализовывать каждую функцию из каркаса, а раскроем их прямо внутри `backtrack()` :
|
||||
|
||||
```src
|
||||
[file]{permutations_i}-[class]{}-[func]{permutations_i}
|
||||
```
|
||||
|
||||
## Учет равных элементов
|
||||
|
||||
!!! question
|
||||
|
||||
Дан массив целых чисел, **который может содержать повторяющиеся элементы**. Верните все неповторяющиеся перестановки.
|
||||
|
||||
Пусть входной массив равен $[1, 1, 2]$ . Чтобы различать два одинаковых элемента $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` цикле), и нужен для того, чтобы равные элементы выбирались только один раз.
|
||||
|
||||
На рисунке ниже показана область действия двух условий обрезки. Помните, что каждый узел дерева соответствует одному выбору, а путь от корня до листа образует одну перестановку.
|
||||
|
||||

|
||||
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,95 @@
|
||||
# Задача о сумме подмножеств
|
||||
|
||||
## Случай без повторяющихся элементов
|
||||
|
||||
!!! question
|
||||
|
||||
Дан массив положительных целых чисел `nums` и целое положительное значение `target` . Найдите все возможные комбинации, сумма элементов которых равна `target` . Во входном массиве нет повторяющихся элементов, и каждый элемент можно выбирать неограниченное число раз. Верните эти комбинации в виде списка; в результате не должно быть повторяющихся комбинаций.
|
||||
|
||||
Например, для входного множества $\{3, 4, 5\}$ и целевого значения $9$ решениями будут $\{3, 3, 3\}$ и $\{4, 5\}$ . При этом нужно обратить внимание на два обстоятельства.
|
||||
|
||||
- Элементы входного множества можно выбирать повторно неограниченное число раз.
|
||||
- Подмножество не различает порядок элементов, поэтому $\{4, 5\}$ и $\{5, 4\}$ считаются одним и тем же подмножеством.
|
||||
|
||||
### Отталкиваемся от решения задачи о перестановках
|
||||
|
||||
Как и в задаче о перестановках, можно представлять построение подмножеств как результат последовательности выборов и во время выбора динамически обновлять "сумму элементов"; когда эта сумма становится равной `target` , соответствующее подмножество записывается в список результатов.
|
||||
|
||||
Однако в отличие от задачи о перестановках **в этой задаче элементы множества можно выбирать неограниченное число раз**, поэтому нам не нужен булев список `selected` для записи того, был ли выбран элемент. Можно слегка изменить код для перестановок и получить первоначальную версию решения:
|
||||
|
||||
```src
|
||||
[file]{subset_sum_i_naive}-[class]{}-[func]{subset_sum_i_naive}
|
||||
```
|
||||
|
||||
Если подать на этот код массив $[3, 4, 5]$ и целевое значение $9$ , то на выходе мы получим $[3, 3, 3], [4, 5], [5, 4]$ . **Хотя все подмножества с суммой $9$ успешно найдены, среди них все же присутствуют дубликаты: $[4, 5]$ и $[5, 4]$** .
|
||||
|
||||
Причина в том, что процесс поиска различает порядок выбора, тогда как для подмножеств порядок не важен. Как показано на рисунке ниже, сначала выбрать $4$ , а затем $5$ , и сначала выбрать $5$ , а затем $4$ - это разные ветви поиска, но им соответствует одно и то же подмножество.
|
||||
|
||||

|
||||
|
||||
Чтобы убрать повторяющиеся подмножества, **одна из прямых идей - удалить дубликаты уже из итогового списка результатов**. Но это решение малоэффективно по двум причинам.
|
||||
|
||||
- Когда массив содержит много элементов, а особенно когда `target` велик, процесс поиска порождает огромное число повторяющихся подмножеств.
|
||||
- Сравнение подмножеств (то есть массивов) само по себе довольно затратно: сначала приходится сортировать массивы, а затем поэлементно сравнивать их.
|
||||
|
||||
### Обрезка повторяющихся подмножеств
|
||||
|
||||
**Поэтому стоит выполнять устранение дубликатов прямо во время поиска, с помощью обрезки**. Посмотрите на рисунок ниже: повторяющиеся подмножества возникают тогда, когда элементы массива выбираются в разном порядке, например так.
|
||||
|
||||
1. Если в первом и втором раундах выбрать соответственно $3$ и $4$ , то будут сгенерированы все подмножества, содержащие эти два элемента, и их можно обозначить как $[3, 4, \dots]$ .
|
||||
2. После этого, если в первом раунде выбрать $4$ , **то во втором раунде нужно пропустить $3$** , потому что подмножества $[4, 3, \dots]$ полностью дублируют подмножества, уже построенные на шаге `1.` .
|
||||
|
||||
Во время поиска выборы на каждом уровне пробуются по одному слева направо, поэтому чем правее ветвь, тем больше ветвей оказывается отсечено.
|
||||
|
||||
1. В первых двух раундах выбираются $3$ и $5$ , что дает подмножества $[3, 5, \dots]$ .
|
||||
2. В первых двух раундах выбираются $4$ и $5$ , что дает подмножества $[4, 5, \dots]$ .
|
||||
3. Если же в первом раунде выбрать $5$ , **то во втором раунде нужно пропустить $3$ и $4$** , потому что подмножества $[5, 3, \dots]$ и $[5, 4, \dots]$ полностью дублируют случаи, описанные в шагах `1.` и `2.` .
|
||||
|
||||

|
||||
|
||||
В общем виде, если входной массив имеет вид $[x_1, x_2, \dots, x_n]$ , а последовательность выборов в ходе поиска равна $[x_{i_1}, x_{i_2}, \dots, x_{i_m}]$ , то она должна удовлетворять условию $i_1 \leq i_2 \leq \dots \leq i_m$ ; **все последовательности выборов, не удовлетворяющие этому условию, приводят к дубликатам и должны отсекаться**.
|
||||
|
||||
### Реализация кода
|
||||
|
||||
Чтобы реализовать такую обрезку, инициализируем переменную `start` , которая будет указывать начальную точку обхода. **После выбора элемента $x_i$ следующий раунд начинается с индекса $i$**. Благодаря этому последовательность выборов всегда удовлетворяет условию $i_1 \leq i_2 \leq \dots \leq i_m$ , а значит, каждое подмножество создается только один раз.
|
||||
|
||||
Помимо этого, мы внесем в код еще два улучшения.
|
||||
|
||||
- Перед началом поиска отсортируем массив `nums` . Тогда при обходе всех вариантов **можно сразу прервать цикл, как только сумма подмножества превысит `target`** , потому что все последующие элементы будут еще больше и их сумма тоже превысит `target` .
|
||||
- Откажемся от отдельной переменной суммы `total` и **будем учитывать сумму через вычитание из `target`** ; когда `target` станет равным $0$ , решение фиксируется.
|
||||
|
||||
```src
|
||||
[file]{subset_sum_i}-[class]{}-[func]{subset_sum_i}
|
||||
```
|
||||
|
||||
На рисунке ниже показан полный процесс backtracking для массива $[3, 4, 5]$ и целевого значения $9$ .
|
||||
|
||||

|
||||
|
||||
## Учет повторяющихся элементов
|
||||
|
||||
!!! question
|
||||
|
||||
Дан массив положительных целых чисел `nums` и целое положительное значение `target` . Найдите все возможные комбинации, сумма элементов которых равна `target` . **Во входном массиве могут присутствовать повторяющиеся элементы, и каждый элемент разрешено выбирать только один раз**. Верните эти комбинации в виде списка; в результате не должно быть повторяющихся комбинаций.
|
||||
|
||||
По сравнению с предыдущей задачей **во входном массиве теперь могут присутствовать повторяющиеся элементы**, и это создает новую проблему. Например, если дан массив $[4, \hat{4}, 5]$ и целевое значение $9$ , то существующий код вернет результат $[4, 5], [\hat{4}, 5]$ , то есть с повторяющимся подмножеством.
|
||||
|
||||
**Причина появления дублей в том, что равные элементы выбираются несколько раз в одном и том же раунде**. На рисунке ниже в первом раунде существует три варианта выбора, и два из них равны $4$ ; из-за этого появляются две дублирующиеся ветви поиска и, соответственно, повторяющиеся подмножества. Точно так же два элемента $4$ во втором раунде тоже порождают дубликаты.
|
||||
|
||||

|
||||
|
||||
### Обрезка равных элементов
|
||||
|
||||
Чтобы решить эту проблему, **нужно ограничить выбор равных элементов так, чтобы в каждом раунде каждый из них выбирался только один раз**. Реализуется это довольно изящно: поскольку массив отсортирован, равные элементы стоят рядом. Значит, если в текущем раунде текущий элемент равен соседнему слева, то этот вариант уже был рассмотрен, и текущий элемент нужно пропустить.
|
||||
|
||||
Одновременно **по условию этой задачи каждый элемент массива можно выбрать только один раз**. К счастью, это ограничение тоже можно реализовать через переменную `start` : после выбора элемента $x_i$ следующий раунд начинается с индекса $i + 1$ . Так мы одновременно убираем повторяющиеся подмножества и исключаем повторный выбор одного и того же элемента.
|
||||
|
||||
### Реализация кода
|
||||
|
||||
```src
|
||||
[file]{subset_sum_ii}-[class]{}-[func]{subset_sum_ii}
|
||||
```
|
||||
|
||||
На рисунке ниже показан процесс backtracking для массива $[4, 4, 5]$ и целевого значения $9$ . В нем используются четыре вида обрезки. Попробуйте сопоставить рисунок с комментариями в коде, чтобы понять полный процесс поиска и то, как работает каждый тип обрезки.
|
||||
|
||||

|
||||
@@ -0,0 +1,23 @@
|
||||
# Резюме
|
||||
|
||||
### Ключевые выводы
|
||||
|
||||
- Алгоритм поиска с возвратом по своей сути является методом полного перебора: он ищет решения путем обхода пространства решений в глубину. Во время поиска он фиксирует решения, удовлетворяющие условиям, пока не найдет все такие решения или пока обход не завершится.
|
||||
- Процесс backtracking состоит из двух частей: попытки и отката. Он с помощью поиска в глубину пробует разные варианты выбора; когда встречается состояние, не удовлетворяющее ограничениям, алгоритм отменяет предыдущий выбор, возвращается к прошлому состоянию и продолжает пробовать другие варианты. Попытка и откат являются двумя противоположными по направлению действиями.
|
||||
- Задачи backtracking обычно содержат несколько ограничений, которые можно использовать для обрезки. Обрезка позволяет заранее завершать ненужные ветви поиска и тем самым значительно повышать эффективность.
|
||||
- Алгоритм backtracking в первую очередь применяется для решения поисковых задач и задач с ограничениями. Задачи комбинаторной оптимизации тоже можно решать с его помощью, но для них часто существуют более эффективные или более подходящие методы.
|
||||
- Задача о перестановках нацелена на поиск всех возможных перестановок элементов данного множества. Мы используем массив для записи того, был ли выбран каждый элемент, и отсекаем ветви, где один и тот же элемент выбирается повторно, чтобы гарантировать однократный выбор каждого элемента.
|
||||
- В задаче о перестановках, если во множестве присутствуют повторяющиеся элементы, в итоговом результате возникнут повторяющиеся перестановки. Поэтому нужно ограничить выбор равных элементов так, чтобы в каждом раунде каждый из них выбирался только один раз; обычно это реализуется с помощью хеш-множества.
|
||||
- Цель задачи о сумме подмножеств - найти все подмножества данного множества, сумма которых равна целевому значению. В множестве порядок элементов не важен, однако процесс поиска порождает результаты во всех возможных порядках, из-за чего появляются повторяющиеся подмножества. Поэтому перед запуском backtracking мы сортируем данные и вводим переменную, указывающую начальную точку обхода в каждом раунде, чтобы отсечь ветви, создающие дубликаты.
|
||||
- В задаче о сумме подмножеств равные элементы массива также порождают повторяющиеся множества. При наличии предварительной сортировки их можно отсекать, проверяя равенство соседних элементов, и тем самым гарантировать, что в каждом раунде равные элементы будут выбираться только один раз.
|
||||
- Задача о $n$ ферзях состоит в поиске способов разместить $n$ ферзей на доске размера $n \times n$ так, чтобы никакие два ферзя не атаковали друг друга. Ограничения этой задачи включают строки, столбцы, главные диагонали и побочные диагонали. Чтобы выполнить ограничение по строкам, используется построчная стратегия размещения, гарантирующая по одному ферзю в каждой строке.
|
||||
- Обработка ограничений по столбцам и диагоналям устроена похожим образом. Для ограничения по столбцам используется массив, фиксирующий наличие ферзя в каждом столбце. Для диагоналей используются два массива, записывающие наличие ферзей на главных и побочных диагоналях. Основная сложность здесь состоит в том, чтобы найти закономерность индексов строк и столбцов клеток, лежащих на одной и той же главной или побочной диагонали.
|
||||
|
||||
### Q & A
|
||||
|
||||
**Q**: Как понять связь между поиском с возвратом и рекурсией?
|
||||
|
||||
В целом backtracking - это скорее "алгоритмическая стратегия", а рекурсия больше похожа на "инструмент".
|
||||
|
||||
- Алгоритмы поиска с возвратом обычно реализуются на основе рекурсии. Однако backtracking - это лишь один из вариантов применения рекурсии, а именно ее использование в поисковых задачах.
|
||||
- Структура рекурсии отражает парадигму разбиения на подзадачи и часто применяется для решения задач divide and conquer, backtracking, динамического программирования (мемоизированной рекурсии) и других подобных задач.
|
||||