mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-25 12:36:07 +00:00
Review the ru version with Codex. (#1870)
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
<u>Алгоритм поиска с возвратом (backtracking algorithm)</u> - это метод решения задач путем полного перебора. Его основная идея состоит в том, чтобы, начиная с некоторого исходного состояния, грубо перебрать все возможные решения, записывать корректные решения и продолжать поиск до тех пор, пока решение не будет найдено или пока не будут исчерпаны все возможные варианты.
|
||||
|
||||
Обычно алгоритмы поиска с возвратом используют "поиск в глубину" для обхода пространства решений. В главе "Бинарные деревья" мы уже упоминали, что прямой, симметричный и обратный обходы относятся к поиску в глубину. Теперь мы на основе прямого обхода построим задачу backtracking и постепенно разберем принцип работы этого алгоритма.
|
||||
Обычно алгоритмы поиска с возвратом используют обход в глубину для обхода пространства решений. В главе "Бинарные деревья" мы уже упоминали, что прямой, симметричный и обратный обходы относятся к обходу в глубину. Теперь мы на основе прямого обхода построим задачу поиска с возвратом и постепенно разберем принцип работы этого алгоритма.
|
||||
|
||||
!!! question "Пример 1"
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
## Попытка и откат
|
||||
|
||||
**Алгоритм называется backtracking, потому что при поиске в пространстве решений он использует стратегию "попытка" и "откат"**. Когда в процессе поиска алгоритм приходит в состояние, из которого нельзя двигаться дальше или нельзя получить удовлетворяющее условиям решение, он отменяет предыдущий выбор, возвращается к более раннему состоянию и пробует другие возможные варианты.
|
||||
**Алгоритм называется поиском с возвратом, потому что при поиске в пространстве решений он использует стратегию "попытка" и "откат"**. Когда в процессе поиска алгоритм приходит в состояние, из которого нельзя двигаться дальше или нельзя получить удовлетворяющее условиям решение, он отменяет предыдущий выбор, возвращается к более раннему состоянию и пробует другие возможные варианты.
|
||||
|
||||
Для примера 1 посещение каждого узла представляет собой "попытку", а прохождение листового узла или возврат к родителю через `return` означает "откат".
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
|
||||
## Обрезка
|
||||
|
||||
Сложные задачи backtracking обычно содержат одно или несколько ограничений, **которые часто можно использовать для "обрезки"**.
|
||||
Сложные задачи поиска с возвратом обычно содержат одно или несколько ограничений, **которые часто можно использовать для "обрезки"**.
|
||||
|
||||
!!! question "Пример 3"
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
[file]{preorder_traversal_iii_compact}-[class]{}-[func]{pre_order}
|
||||
```
|
||||
|
||||
Термин "обрезка" очень нагляден. Как показано на рисунке ниже, во время поиска **мы "срезаем" ветви поиска, не удовлетворяющие ограничениям** , тем самым избегая множества бессмысленных попыток и повышая эффективность поиска.
|
||||
Термин "обрезка" очень нагляден. Как показано на рисунке ниже, во время поиска **мы отсекаем ветви, не удовлетворяющие ограничениям** , тем самым избегая множества бессмысленных попыток и повышая эффективность поиска.
|
||||
|
||||

|
||||
|
||||
@@ -439,13 +439,13 @@
|
||||
|
||||

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

|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
|
||||
Дан массив целых чисел, в котором нет повторяющихся элементов. Верните все возможные перестановки.
|
||||
|
||||
С точки зрения backtracking **процесс построения перестановок можно представить как результат последовательности выборов**. Пусть входной массив равен $[1, 2, 3]$ ; если мы сначала выберем $1$ , затем $3$ , а потом $2$ , то получим перестановку $[1, 3, 2]$ . Откат означает отмену одного из выборов с последующей попыткой других вариантов.
|
||||
С точки зрения поиска с возвратом **процесс построения перестановок можно представить как результат последовательности выборов**. Пусть входной массив равен $[1, 2, 3]$ ; если мы сначала выберем $1$ , затем $3$ , а потом $2$ , то получим перестановку $[1, 3, 2]$ . Откат здесь означает отмену одного из выборов с последующей попыткой других вариантов.
|
||||
|
||||
С точки зрения кода backtracking множество кандидатов `choices` состоит из всех элементов входного массива, а состояние `state` - из элементов, уже выбранных к текущему моменту. Обратите внимание, что каждый элемент разрешено выбирать только один раз, **поэтому все элементы в `state` должны быть уникальны**.
|
||||
С точки зрения кода поиска с возвратом множество кандидатов `choices` состоит из всех элементов входного массива, а состояние `state` - из элементов, уже выбранных к текущему моменту. Поскольку каждый элемент разрешено выбирать только один раз, **все элементы в `state` должны быть уникальны**.
|
||||
|
||||
Как показано на рисунке ниже, процесс поиска можно развернуть в дерево рекурсии, где каждый узел представляет текущее состояние `state` . Начиная от корня, после трех раундов выбора мы попадаем в листья, и каждый лист соответствует одной перестановке.
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
### Реализация кода
|
||||
|
||||
После прояснения всей логики можно просто "заполнить пропуски" в шаблоне backtracking. Чтобы сократить общий объем кода, мы не будем отдельно реализовывать каждую функцию из каркаса, а раскроем их прямо внутри `backtrack()` :
|
||||
После прояснения всей логики можно просто "заполнить пропуски" в шаблоне поиска с возвратом. Чтобы сократить общий объем кода, мы не будем отдельно реализовывать каждую функцию из каркаса, а раскроем их прямо внутри `backtrack()` :
|
||||
|
||||
```src
|
||||
[file]{permutations_i}-[class]{}-[func]{permutations_i}
|
||||
@@ -67,7 +67,7 @@
|
||||
|
||||
Точно так же, если в первом раунде выбрать $2$ , то во втором раунде выборы $1$ и $\hat{1}$ снова создадут дублирующиеся ветви, поэтому и в этом случае ветвь $\hat{1}$ нужно отсечь.
|
||||
|
||||
По своей сути **наша цель заключается в том, чтобы на каждом раунде выбора каждый из нескольких равных элементов выбирался только один раз**.
|
||||
Иначе говоря, **наша цель заключается в том, чтобы на каждом раунде выбора каждый из нескольких равных элементов выбирался только один раз**.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
Дан массив положительных целых чисел `nums` и целое положительное значение `target` . Найдите все возможные комбинации, сумма элементов которых равна `target` . Во входном массиве нет повторяющихся элементов, и каждый элемент можно выбирать неограниченное число раз. Верните эти комбинации в виде списка; в результате не должно быть повторяющихся комбинаций.
|
||||
|
||||
Например, для входного множества $\{3, 4, 5\}$ и целевого значения $9$ решениями будут $\{3, 3, 3\}$ и $\{4, 5\}$ . При этом нужно обратить внимание на два обстоятельства.
|
||||
Например, для входного множества $\{3, 4, 5\}$ и целевого значения $9$ решениями будут $\{3, 3, 3\}$ и $\{4, 5\}$ . При этом важно учитывать два обстоятельства.
|
||||
|
||||
- Элементы входного множества можно выбирать повторно неограниченное число раз.
|
||||
- Подмножество не различает порядок элементов, поэтому $\{4, 5\}$ и $\{5, 4\}$ считаются одним и тем же подмножеством.
|
||||
@@ -39,7 +39,7 @@
|
||||
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]$ .
|
||||
@@ -62,9 +62,9 @@
|
||||
[file]{subset_sum_i}-[class]{}-[func]{subset_sum_i}
|
||||
```
|
||||
|
||||
На рисунке ниже показан полный процесс backtracking для массива $[3, 4, 5]$ и целевого значения $9$ .
|
||||
На рисунке ниже показан полный процесс поиска с возвратом для массива $[3, 4, 5]$ и целевого значения $9$ .
|
||||
|
||||

|
||||

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

|
||||

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