mirror of
https://github.com/krahets/hello-algo.git
synced 2026-05-04 19:02:47 +08:00
862 lines
72 KiB
Markdown
862 lines
72 KiB
Markdown
# Поиск с возвратом
|
||
|
||
{width="3.630207786526684in" height="4.697916666666667in"}
|
||
|
||
#### Алгоритмы поиска с возвратом
|
||
|
||
> *Алгоритм поиска с возвратом* -- это метод решения задач путем перебора. Его основная идея заключается в том, чтобы, начиная с начального состояния, осуществлять грубый поиск всех возможных решений, фиксируя правильное найденное решение. Процесс поиска продолжается до тех пор, пока не будет найдено решение или не будут исчерпаны все возможные варианты.
|
||
>
|
||
> Алгоритмы поиска с возвратом обычно используют поиск в глубину для об- хода пространства решений. В разделе «Двоичные деревья» упоминалось, что прямой, симметричный и обратный обходы относятся к поиску в глубину. Да- лее, используя прямой обход, мы реализуем задачу поиска с возвратом, чтобы постепенно понять принцип работы этого алгоритма.
|
||
>
|
||
> Для решения этой задачи мы выполняем предварительный обход дерева и проверяем, равно ли значение текущего узла 7. Если равно, то добавляем зна- чение этого узла в список результатов res. Процесс представлен на рис. 13.1 и в следующем коде.
|
||
>
|
||
> \# === File: preorder_traversal_i_compact.py === def pre_order(root: TreeNode):
|
||
>
|
||
> \"\"\" Предварительный обход: пример 1.\"\"\" if root is None:
|
||
>
|
||
> return
|
||
>
|
||
> if root.val == 7:
|
||
>
|
||
> \# Запись решения. res.append(root)
|
||
>
|
||
> pre_order(root.left)
|
||
>
|
||
> pre_order(root.right)
|
||
>
|
||
> Поиск в глубину
|
||
>
|
||
> Прямой порядок обхода Посетить узел в
|
||
>
|
||
> **Выполнить прямой обход двоичного дерева и записать узлы со значением 7**
|
||
>
|
||
> **Рис. 13.1.** Поиск узлов в предварительном обходе
|
||
|
||
1. **Попытка и возврат**
|
||
|
||
> **Алгоритм называется поиском с возвратом, потому что при поиске в пространстве решений он использует стратегию попытки и возврата**. Когда алгоритм сталкивается с состоянием, в котором невозможно продолжать или невозможно получить удовлетворительное решение, он отменяет преды- дущий выбор, возвращается к предыдущему состоянию и пробует другие воз- можные варианты.
|
||
>
|
||
> В примере 1 посещение каждого узла представляет собой попытку, а пере- ход через листовой узел или возврат к родительскому узлу через return озна- чает возврат.
|
||
>
|
||
> Стоит отметить, что **откат включает не только возврат функции**. Чтобы объяснить это, мы немного расширим пример 1.
|
||
>
|
||
> Возьмем за основу код для примера 1. Нам потребуется добавить список path для записи пути посещенных узлов. Когда будет найден узел со значением 7, скопируем path и добавим его в список результатов res. После завершения об- хода res будет содержать все решения. Код реализации представлен ниже.
|
||
>
|
||
> \# === File: preorder_traversal_ii_compact.py === def pre_order(root: TreeNode):
|
||
>
|
||
> \"\"\" Предварительный обход: пример 2.\"\"\" if root is None:
|
||
>
|
||
> return \# Попытка.
|
||
>
|
||
> path.append(root) if root.val == 7:
|
||
>
|
||
> \# Запись решения. res.append(list(path))
|
||
>
|
||
> pre_order(root.left)
|
||
>
|
||
> pre_order(root.right) \# Возврат.
|
||
>
|
||
> path.pop()
|
||
>
|
||
> В каждой попытке мы добавляем текущий узел в path для записи пути. Перед возвратом необходимо удалить этот узел из path, чтобы **восстановить состо- яние до этой попытки**.
|
||
>
|
||
> Изучив процесс выполнения алгоритма на рис. 13.2, **можно представить попытку и возврат как движение вперед и отмену**, как два противополож- ных действия.
|
||
>
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 13.2.** Попытка и возврат. Шаги 1--3
|
||
>
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 13.2.** *Продолжение*. Шаги 4--6
|
||
>
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 13.2.** *Продолжение*. Шаги 7--9
|
||
>
|
||
> 
|
||
|
||

|
||
|
||
### Обрезка
|
||
|
||
> **Рис. 13.2.** *Окончание*. Шаги 10--11
|
||
>
|
||
> Сложные задачи поиска с возвратом обычно содержат одно или несколько ограничений, **которые можно использовать для обрезки**.
|
||
>
|
||
> Для выполнения данного условия **требуется добавить операцию обрезки**: в процессе поиска, если встречается узел со значением 3, следует немедленно вернуться, не продолжая поиск. Код реализации представлен ниже.
|
||
>
|
||
> \# === File: preorder_traversal_iii_compact.py ===
|
||
>
|
||
> def pre_order(root: TreeNode):
|
||
>
|
||
> \"\"\" Предварительный обход: пример 3.\"\"\" \# Обрезка.
|
||
>
|
||
> if root is None or root.val == 3: return
|
||
>
|
||
> \# Попытка.
|
||
>
|
||
> path.append(root) if root.val == 7:
|
||
>
|
||
> \# Запись решения. res.append(list(path))
|
||
>
|
||
> pre_order(root.left) pre_order(root.right) \# Возврат.
|
||
>
|
||
> path.pop()
|
||
>
|
||
> Обрезка является очень наглядным термином. В процессе поиска **мы обре- заем ветви поиска**, **не удовлетворяющие заданным условиям**, и избегаем множества бессмысленных попыток, тем самым повышая эффективность по- иска, как показано на рис. 13.3.
|
||
|
||

|
||
|
||
> **Рис. 13.3.** Обрезка в соответствии с заданными условиями
|
||
|
||
### Каркас кода
|
||
|
||
> Далее мы попытаемся сформировать основной каркас операций «попытка, возврат, обрезка» для повышения универсальности кода.
|
||
>
|
||
> В следующем каркасе кода state обозначает текущее состояние задачи, а choices -- возможные выборы в текущем состоянии:
|
||
>
|
||
> 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)
|
||
>
|
||
> Теперь на основе каркаса кода решим пример 3. Состояние state -- это путь обхода узлов, выбор choices -- это левый и правый дочерние узлы текущего узла, результат res -- список путей.
|
||
>
|
||
> \# === File: preorder_traversal_iii_template.py === def is_solution(state: list\[TreeNode\]) -\> bool:
|
||
>
|
||
> \"\"\" Проверка, является ли текущее состояние решением.\"\"\" return state and state\[-1\].val == 7
|
||
>
|
||
> def record_solution(state: list\[TreeNode\], res: list\[list\[TreeNode\]\]): \"\"\" Запись решения.\"\"\"
|
||
>
|
||
> res.append(list(state))
|
||
>
|
||
> def is_valid(state: list\[TreeNode\], choice: TreeNode) -\> bool: \"\"\" Проверка легитимности выбора в текущем состоянии.\"\"\" return choice is not None and choice.val != 3
|
||
>
|
||
> def make_choice(state: list\[TreeNode\], choice: TreeNode): \"\"\" Обновление состояния.\"\"\"
|
||
>
|
||
> state.append(choice)
|
||
>
|
||
> def undo_choice(state: list\[TreeNode\], choice: TreeNode): \"\"\" Восстановление состояния.\"\"\"
|
||
>
|
||
> state.pop()
|
||
>
|
||
> def backtrack(
|
||
>
|
||
> state: list\[TreeNode\], choices: list\[TreeNode\], res: list\[list\[TreeNode\]\]
|
||
>
|
||
> ):
|
||
>
|
||
> \"\"\" Поиск с возвратом: пример 3.\"\"\"
|
||
>
|
||
> \# Проверка, является ли состояние решением. if is_solution(state):
|
||
>
|
||
> \# Запись решения. record_solution(state, res)
|
||
>
|
||
> \# Перебор всех вариантов. for choice in choices:
|
||
>
|
||
> \# Обрезка: проверка легитимности выбора. if is_valid(state, choice):
|
||
>
|
||
> \# Попытка: сделать выбор, обновить состояние. make_choice(state, choice)
|
||
>
|
||
> \# Переход к следующему выбору.
|
||
>
|
||
> backtrack(state, \[choice.left, choice.right\], res)
|
||
>
|
||
> \# Возврат: отмена выбора, возврат к предыдущему состоянию. undo_choice(state, choice)
|
||
>
|
||
> Согласно условию задачи после нахождения узла со значением 7 необходи- мо продолжать поиск, **поэтому следует удалить оператор** return **после запи- си решения**. На рис. 13.4 сравнивается процесс поиска с сохранением и удале- нием оператора return.
|
||
>
|
||
> После записи решения остановить поиск
|
||
>
|
||
> **Сохранение return**
|
||
>
|
||
> Возврат после записи решения, не продолжать поиск
|
||
>
|
||
> **Удаление return**
|
||
>
|
||
> Не возвращаться после записи решения, продолжить поиск
|
||
>
|
||
> **Рис. 13.4.** Сравнение процесса поиска с сохранением и удалением return
|
||
>
|
||
> По сравнению с реализацией на основе предварительного обхода, реали- зация на основе каркаса поиска с возвратом выглядит более громоздкой, но обладает большей универсальностью. На самом деле многие задачи поиска с возвратом можно решить в рамках этого каркаса. Необходимо лишь опре- делить state и choices в соответствии с конкретной задачей и реализовать ме- тоды каркаса.
|
||
|
||
### Основные термины
|
||
|
||
> Для более четкого понимания алгоритмических задач мы систематизируем значения часто используемых терминов обратного поиска и приведем соот- ветствующие примеры для задачи 3, как показано в табл. 13.1.
|
||
>
|
||
> **Таблица 13.1.** Основные термины обратного поиска
|
||
|
||
+---------------+----------------------------------------+-----------------------------------------+
|
||
| > **Термин** | > **Определение** | > **Пример 3** |
|
||
+===============+========================================+=========================================+
|
||
| > Решение | > Ответ, удовлетворяющий определен- | > Все пути от корневого узла до узла 7, |
|
||
+---------------+----------------------------------------+-----------------------------------------+
|
||
| | > ным условиям задачи, может быть одно | > удовлетворяющие условиям |
|
||
+---------------+----------------------------------------+-----------------------------------------+
|
||
| | > или несколько решений | |
|
||
+---------------+----------------------------------------+-----------------------------------------+
|
||
| > Ограничение | > Ограничение на допустимость реше- | > Путь не содержит узлов со значением 3 |
|
||
+---------------+----------------------------------------+-----------------------------------------+
|
||
| | > ния, обычно используется для обрезки | |
|
||
+---------------+----------------------------------------+-----------------------------------------+
|
||
| > Состояние | > Ситуация задачи в определенный мо- | > Текущий посещенный путь узлов, т. е. |
|
||
+---------------+----------------------------------------+-----------------------------------------+
|
||
| | > мент, включая сделанные выборы | > список узлов path |
|
||
+---------------+----------------------------------------+-----------------------------------------+
|
||
| > Попытка | > Процесс исследования пространства | > Рекурсивный доступ к левому (право- |
|
||
+---------------+----------------------------------------+-----------------------------------------+
|
||
| | > решений на основе доступных вы- | > му) дочернему узлу, добавление узла |
|
||
+---------------+----------------------------------------+-----------------------------------------+
|
||
| | > боров, включая выбор, обновление | > в path, проверка значения узла на |
|
||
+---------------+----------------------------------------+-----------------------------------------+
|
||
| | > состояния, проверку на решение | > равенство 7 |
|
||
+---------------+----------------------------------------+-----------------------------------------+
|
||
| > Возврат | > Отмена предыдущих выборов и воз- | > При переходе через листовой узел, |
|
||
+---------------+----------------------------------------+-----------------------------------------+
|
||
| | > врат к предыдущему состоянию при | > завершении посещения узла, встрече |
|
||
+---------------+----------------------------------------+-----------------------------------------+
|
||
| | > встрече состояния, не удовлетворяю- | > узла со значением 3 поиск прекраща- |
|
||
+---------------+----------------------------------------+-----------------------------------------+
|
||
| | > щего ограничению | > ется, происходит выход из функции |
|
||
+---------------+----------------------------------------+-----------------------------------------+
|
||
| > Обрезка | > Метод избегания бессмысленных путей | > При встрече узла со значением 3 по- |
|
||
+---------------+----------------------------------------+-----------------------------------------+
|
||
| | > поиска на основе условий и ограниче- | > иск прекращается |
|
||
+---------------+----------------------------------------+-----------------------------------------+
|
||
| | > ний задачи, повышающий эффектив- | |
|
||
+---------------+----------------------------------------+-----------------------------------------+
|
||
| | > ность поиска | |
|
||
+---------------+----------------------------------------+-----------------------------------------+
|
||
|
||
### Преимущества и ограничения
|
||
|
||
> Алгоритм поиска с возвратом, по сути, является алгоритмом поиска в глуби- ну, который пытается найти все возможные решения до тех пор, пока не будет найдено решение, удовлетворяющее условиям. Преимущество этого метода заключается в возможности нахождения всех возможных решений, и при раз- умной обрезке он обладает высокой эффективностью.
|
||
>
|
||
> Однако при решении крупных или сложных задач **эффективность работы алгоритма возврата может оказаться неприемлемой.**
|
||
|
||
- **Время**: алгоритм поиска с возвратом обычно требует перебора всех воз- можных состояний пространства, и временная сложность может дости- гать экспоненциального или факториального порядка.
|
||
|
||
- **Пространство**: в рекурсивных вызовах необходимо сохранять текущее состояние (например, путь, вспомогательные переменные для обрезки и т. д.), и при большой глубине потребность в пространстве может стать значительной.
|
||
|
||
> Тем не менее **алгоритм поиска с возвратом по-прежнему является наи- лучшим решением для некоторых задач поиска и задач с ограничени- ями**. В этих задачах невозможно предсказать, какие выборы могут привести к эффективному решению, поэтому необходимо перебирать все возможные варианты. В таких случаях ключевым моментом является оптимизация эф- фективности, и существуют две распространенные стратегии.
|
||
|
||
- **Обрезка**: избегание поиска по путям, которые заведомо не приведут к решению, что позволяет сэкономить время и пространство.
|
||
|
||
- **Эвристический поиск**: введение некоторых стратегий или оценочных значений в процессе поиска, чтобы в первую очередь исследовать пути, которые с наибольшей вероятностью могут привести к эффективному решению.
|
||
|
||
### Типичные задачи поиска с возвратом
|
||
|
||
> Алгоритм поиска с возвратом можно использовать для решения множества за- дач поиска, задач с ограничениями и задач комбинаторной оптимизации.
|
||
>
|
||
> **Задачи поиска**: цель этих задач -- найти решение, удовлетворяющее опре- деленным условиям.
|
||
|
||
- Задача о перестановках: дано множество, требуется найти все возмож- ные перестановки элементов.
|
||
|
||
- Задача о сумме подмножеств: дано множество и целевая сумма, необхо- димо найти все подмножества, сумма которых равна целевой.
|
||
|
||
- Задача о Ханойских башнях: даны три стержня и несколько дисков раз- ного размера, требуется переместить все диски с одного стержня на дру- гой, перемещая по одному диску за раз, при этом нельзя класть больший диск на меньший.
|
||
|
||
> **Задачи с ограничениями**: цель этих задач -- найти решение, удовлетворя- ющее всем ограничениям.
|
||
|
||
- Задача об *n* ферзях: разместить *n* ферзей на шахматной доске размером
|
||
|
||
> *n*×*n* так, чтобы они не рубили друг друга.
|
||
|
||
- Судоку: заполнить числами от 1 до 9 сетку 9×9 так, чтобы в каждой стро- ке, каждом столбце и каждой подгруппе 3×3 числа не повторялись.
|
||
|
||
- Задача о раскраске графа: дан неориентированный граф, требуется рас- красить его вершины минимальным числом цветов так, чтобы соседние вершины имели разные цвета.
|
||
|
||
> **Задачи комбинаторной оптимизации**: цель этих задач -- найти опти- мальное решение в комбинаторном пространстве, удовлетворяющее опреде- ленным условиям.
|
||
|
||
- Задача о рюкзаке 0-1: дано множество предметов и рюкзак. Каждый предмет имеет определенную ценность и вес, требуется выбрать пред- меты так, чтобы их общая ценность была максимальной при ограничен- ной вместимости рюкзака.
|
||
|
||
- Задача коммивояжера: начиная с некоторой вершины графа, требуется посетить все остальные вершины ровно один раз и вернуться в началь- ную. Найдя при этом кратчайший путь.
|
||
|
||
- Задача о максимальной клике: дан неориентированный граф, требуется найти максимальный полный подграф, т. е. подграф, в котором любые две вершины соединены ребром.
|
||
|
||
> Следует отметить, что для многих задач комбинаторной оптимизации алго- ритм поиска с возвратом не является оптимальным решением.
|
||
|
||
- Задача о рюкзаке 0-1 обычно решается с помощью динамического программирования для достижения более высокой временной эффек- тивности.
|
||
|
||
- Задача коммивояжера является известной NP-трудной задачей, для ее решения часто используются генетические и муравьиные алгоритмы.
|
||
|
||
- Задача о максимальной клике является классической задачей теории графов и может быть решена с помощью жадных алгоритмов или других эвристических методов.
|
||
|
||
#### задача о перестановках
|
||
|
||
> Задача о перестановках является типичным примером применения алгорит- ма поиска с возвратом. Она определяется как задача нахождения всех воз- можных перестановок элементов в заданном множестве (например, массиве или строке).
|
||
>
|
||
> В табл. 13.2 представлено несколько примеров данных, включая входной массив и все соответствующие перестановки.
|
||
>
|
||
> **Таблица 13.2.** Примеры полных перестановок
|
||
|
||
+----------------------+--------------------------------------------------------------------------------+
|
||
| > **Входной массив** | > **Все перестановки** |
|
||
+======================+================================================================================+
|
||
| > \[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\] |
|
||
+----------------------+--------------------------------------------------------------------------------+
|
||
|
||
### Случай без равных элементов
|
||
|
||
> С точки зрения алгоритма поиска с возвратом **процесс генерации пере- становок можно представить как результат серии выборов**. Предполо- жим, что входной массив равен \[1, 2, 3\]. Если сначала выбрать 1, затем 3, а в
|
||
>
|
||
> конце 2, то получится перестановка \[1, 3, 2\]. Возврат означает отмену выбора и продолжение попыток других вариантов.
|
||
>
|
||
> С точки зрения кода реализации поиска с возвратом множество кандидатов
|
||
>
|
||
> choices -- это все элементы входного массива, а состояние state -- это элементы,
|
||
>
|
||
> выбранные до текущего момента. Следует отметить, что каждый элемент мо- жет быть выбран только один раз, **поэтому все элементы в** state **должны быть уникальными**.
|
||
>
|
||
> Процесс поиска можно развернуть в виде рекурсивного дерева, в котором каждый узел представляет текущее состояние state, как показано на рис. 13.5. Начиная с корневого узла, после трех раундов выбора достигается листовой узел, а каждый листовой узел соответствует одной перестановке.
|
||
|
||

|
||
|
||
> **Рис. 13.5.** Рекурсивное дерево полных перестановок
|
||
|
||
##### Обрезка повторного выбора
|
||
|
||
> Чтобы обеспечить выбор каждого элемента только один раз, вводится булев массив selected, где selected\[i\] указывает, был ли выбран элемент choices\[i\], и на его основе выполняется следующая обрезка.
|
||
|
||
- После выбора choice\[i\] значение selected\[i\] устанавливается в True, т. е. элемент помечается как выбранный.
|
||
|
||
- При обходе списка выбора choices пропускаются все уже выбранные узлы, т. е. выполняется обрезка.
|
||
|
||
> Предположим, что в первом раунде выбирается 1, во втором -- 3, а в тре- тьем -- 2, тогда во втором раунде необходимо обрезать ветвь элемента 1, а в третьем -- ветви элементов 1 и 3, как показано на рис. 13.6.
|
||
>
|
||
> Из рис. 13.6 видно, что это отсечение уменьшает размер пространства поис- ка с *O*(*nn*) до *O*(*n*!).
|
||
>
|
||
> 
|
||
>
|
||
> **Рис. 13.6.** Пример обрезки в задаче полных перестановок
|
||
|
||
##### Код реализации
|
||
|
||
> На основе вышеизложенное можно заполнить пробелы в каркасе кода. Чтобы сократить код, функции каркаса кода не реализуются отдельно, а объединены в функцию backtrack().
|
||
>
|
||
> \# === File: permutations_i.py === def backtrack(
|
||
>
|
||
> state: list\[int\], choices: list\[int\], selected: list\[bool\], res:
|
||
>
|
||
> list\[list\[int\]\]
|
||
>
|
||
> ):
|
||
>
|
||
> \"\"\" Поиск с возвратом: полные перестановки I.\"\"\"
|
||
>
|
||
> \# Когда длина состояния равна количеству элементов, фиксируется решение. if len(state) == len(choices):
|
||
>
|
||
> res.append(list(state)) return
|
||
>
|
||
> \# Обход всех выборов.
|
||
>
|
||
> for i, choice in enumerate(choices):
|
||
>
|
||
> \# Обрезка: не допускается повторный выбор элементов. if not selected\[i\]:
|
||
>
|
||
> \# Попытка: сделать выбор, обновить состояние. selected\[i\] = True
|
||
>
|
||
> state.append(choice)
|
||
>
|
||
> \# Переход к следующему выбору. backtrack(state, choices, selected, res)
|
||
>
|
||
> \# Возврат: отмена выбора, восстановление предыдущего состояния. selected\[i\] = False
|
||
>
|
||
> state.pop()
|
||
>
|
||
> def permutations_i(nums: list\[int\]) -\> list\[list\[int\]\]: \"\"\" Полные перестановки.\"\"\"
|
||
>
|
||
> res = \[\]
|
||
>
|
||
> backtrack(state=\[\], choices=nums, selected=\[False\] \* len(nums), res=res) return res
|
||
|
||
### Учет равных элементов
|
||
|
||
> Предположим, что входной массив равен \[1, 1, 2\]. Для удобства различения двух повторяющихся элементов 1 второй 1 обозначим как 1\^.
|
||
>
|
||
> Как видно на рис. 13.7, половина перестановок, сгенерированных вышеука- занным методом, являются одинаковыми.
|
||
|
||

|
||
|
||
> **Рис. 13.7.** Повторяющиеся перестановки
|
||
>
|
||
> Как же избавиться от повторяющихся перестановок? Самый прямой спо- соб -- использовать хеш-набор для удаления дубликатов из результата пере- становок. Однако это не самый изящный подход, **так как ветви поиска, гене- рирующие повторяющиеся перестановки**, **излишни**, **и их нужно заранее распознавать и обрезать** -- это повысит эффективность алгоритма.
|
||
|
||
##### Обрезка равных элементов
|
||
|
||
> В первом раунде выбор 1 или 1\^ эквивалентен, так как все перестановки, сге- нерированные под этими двумя выборами, повторяются, как показано на рис. 13.8. Поэтому элемент 1\^ нужно обрезать.
|
||
>
|
||
> Аналогично после выбора 2 в первом раунде выбор 1 и 1\^ во втором раун- де также создадут повторяющиеся ветви, поэтому 1\^ во втором раунде также нужно обрезать.
|
||
|
||
###### По сути, наша цель -- убедиться, что в каждом раунде выбора несколько равных элементов будут выбраны только один раз.
|
||
|
||

|
||
|
||
> **Рис. 13.8.** Обрезка повторяющихся перестановок
|
||
|
||
##### Код реализации
|
||
|
||
> Возьмем за основу код из предыдущей задачи. В каждом раунде выбора вве- дем хеш-набор duplicated, который будет использоваться для записи элемен- тов, уже проверенных в этом раунде, и для обрезки повторяющихся элементов.
|
||
>
|
||
> \# === File: permutations_ii.py === def backtrack(
|
||
>
|
||
> state: list\[int\], choices: list\[int\], selected: list\[bool\], res: list\[list\[int\]\]
|
||
>
|
||
> ):
|
||
>
|
||
> \"\"\" Поиск с возвратом: полные перестановки II.\"\"\"
|
||
>
|
||
> \# Когда длина состояния равна количеству элементов, решение фиксируется. if len(state) == len(choices):
|
||
>
|
||
> res.append(list(state)) return
|
||
>
|
||
> \# Обход всех выборов. duplicated = set\[int\]()
|
||
>
|
||
> for i, choice in enumerate(choices):
|
||
>
|
||
> \# Обрезка: не допускается повторный выбор элементов и выбор равных элементов if not selected\[i\] and choice not in duplicated:
|
||
>
|
||
> \# Попытка: сделать выбор, обновить состояние. duplicated.add(choice) \# Запись выбранного значения элемента.
|
||
>
|
||
> selected\[i\] = True state.append(choice)
|
||
>
|
||
> \# Переход к следующему выбору. backtrack(state, choices, selected, res)
|
||
>
|
||
> \# Возврат: отмена выбора, восстановление предыдущего состояния. selected\[i\] = False
|
||
>
|
||
> state.pop()
|
||
>
|
||
> def permutations_ii(nums: list\[int\]) -\> list\[list\[int\]\]: \"\"\" Полные перестановки II.\"\"\"
|
||
>
|
||
> res = \[\]
|
||
>
|
||
> backtrack(state=\[\], choices=nums, selected=\[False\] \* len(nums), res=res) return res
|
||
>
|
||
> Предположим, что элементы попарно различны, тогда *n* элементов имеют *n*! перестановок (факториал). При записи результата необходимо скопировать список длиной *n* за время *O*(*n*). Таким образом, **временная сложность со- ставляет** *O*(*n*!*n*).
|
||
>
|
||
> Максимальная глубина рекурсии равна *n*, используется *O*(*n*) пространства стека вызовов. Для selected требуется *O*(*n*) пространства. В любой момент времени в duplicated может содержаться максимум *n* элементов, что соответ- ствует *O*(*n*2) пространства. Таким образом, **пространственная сложность составляет** *O*(*n*2).
|
||
|
||
##### Сравнение двух видов обрезки
|
||
|
||
> Обратите внимание, что, хотя и selected, и duplicated используются для обрез- ки, их цели различны.
|
||
|
||
- **Обрезка повторного выбора**: в процессе всего поиска существует только один массив selected. В нем фиксируется элементы, включен- ные в текущее состояние, а его цель -- избежать повторного появления элемента в state.
|
||
|
||
- **Обрезка равных элементов**: каждый раунд выбора (каждый вызов функции backtrack) включает один хеш-набор duplicated. Он фиксирует, какие элементы были выбраны в текущем обходе (цикл for), а его цель -- гарантировать, что равные элементы выбираются только один раз.
|
||
|
||
> На рис. 13.9 демонстрируется область действия двух условий обрезки. Об- ратите внимание, что каждый узел в дереве представляет собой выбор, а узлы на пути от корня до листа составляют одну перестановку.
|
||
>
|
||
> 
|
||
>
|
||
> **Рис. 13.9.** Область действия двух условий обрезки
|
||
|
||
1. **задача о сумме подмножеств**
|
||
|
||
### Случай без повторяющихся элементов
|
||
|
||
> Например, для входного множества {3, 4, 5} и целевого числа 9 решениями
|
||
>
|
||
> будут {3, 3, 3}, {4, 5}. Следует обратить внимание на следующие два момента:
|
||
|
||
- элементы входного множества можно выбирать неограниченное коли- чество раз;
|
||
|
||
- порядок элементов в подмножестве не имеет значения, например {4, 5} и {5, 4} -- одно и то же подмножество.
|
||
|
||
##### Сравнение с решением задачи о полных перестановках
|
||
|
||
> Подобно задаче о полных перестановках, процесс генерации подмножеств можно представить как серию выборов, а в процессе выбора в реальном вре- мени обновлять сумму элементов. Когда сумма элементов равна target, под- множество записывается в список результатов.
|
||
>
|
||
> Однако, в отличие от задачи о полных перестановках, **в данной задаче элементы множества можно выбирать неограниченное количество раз**,
|
||
>
|
||
> поэтому нет необходимости использовать булев список selected для записи выбранных элементов. Для получения начального решения можно просто не- много изменить код для полных перестановок.
|
||
>
|
||
> \# === File: subset_sum_i_naive.py === def backtrack(
|
||
>
|
||
> state: list\[int\], target: int, total: int,
|
||
>
|
||
> choices: list\[int\], res: list\[list\[int\]\],
|
||
>
|
||
> ):
|
||
>
|
||
> \"\"\" Поиск с возвратом: сумма подмножеств I.\"\"\"
|
||
>
|
||
> \# Если сумма подмножества равна target, записать решение. if total == target:
|
||
>
|
||
> res.append(list(state)) return
|
||
>
|
||
> \# Перебор всех вариантов выбора. for i in range(len(choices)):
|
||
>
|
||
> \# Обрезка: если сумма подмножества превышает target, пропустить этот выбор. if total + choices\[i\] \> target:
|
||
>
|
||
> continue
|
||
>
|
||
> \# Попытка: сделать выбор, обновить сумму элементов total. state.append(choices\[i\])
|
||
>
|
||
> \# Переход к следующему выбору.
|
||
>
|
||
> backtrack(state, target, total + choices\[i\], choices, res)
|
||
>
|
||
> \# Возврат: отмена выбора, восстановление предыдущего состояния. state.pop()
|
||
>
|
||
> def subset_sum_i_naive(nums: list\[int\], target: int) -\> list\[list\[int\]\]: \"\"\" Решение задачи о сумме подмножеств I (включая повторяющиеся
|
||
>
|
||
> подмножества).\"\"\"
|
||
>
|
||
> state = \[\] \# Состояние (подмножество). total = 0 \# Сумма подмножества.
|
||
>
|
||
> res = \[\] \# Список результатов (список подмножеств). backtrack(state, target, total, nums, res)
|
||
>
|
||
> return res
|
||
>
|
||
> При вводе в этот код массива \[3, 4, 5\] и целевого элемента 9 будет выведено \[3, 3, 3\], \[4, 5\], \[5, 4\]. **Хотя удалось найти все подмножества с суммой 9**, **среди**
|
||
>
|
||
> **них есть повторяющиеся подмножества** \[4, 5\] **и** \[5, 4\].
|
||
>
|
||
> Это происходит потому, что процесс поиска различает порядок выбора, тогда как в подмножествах порядок элементов не важен. Как показано на рис. 13.10, сначала выбрать 4, а затем 5 и сначала выбрать 5, а затем 4 -- это разные ветви, но они соответствуют одному и тому же подмножеству.
|
||
>
|
||
> 
|
||
>
|
||
> **Рис. 13.10.** Поиск подмножеств и обрезка по превышению целевого значения
|
||
>
|
||
> **Одним из очевидных подходов к устранению повторяющихся подмно- жеств является удаление дубликатов из списка результатов**. Однако этот метод очень неэффективен по двум причинам.
|
||
|
||
- Когда в массиве много элементов, особенно когда значение target ве- лико, процесс поиска генерирует множество повторяющихся подмно- жеств.
|
||
|
||
- Сравнение подмножеств (массивов) на различия очень затратная по вре- мени операция. Она требует сначала сортировки массивов, затем срав- нения различий каждого элемента в массиве.
|
||
|
||
##### Обрезка повторяющихся подмножеств
|
||
|
||
> Рассмотрим устранение дубликатов в процессе поиска с помощью обрез- ки. На рис. 13.11 показано, что повторяющиеся подмножества возникают при выборе элементов массива в разном порядке, например в следующих случаях:
|
||
|
||
1) пусть на первом и втором этапах выбираются 3 и 4 соответственно, соз- даются все подмножества, содержащие эти два элемента, обозначенные как \[3, 4, ...\];
|
||
|
||
2) затем если на первом этапе выбирается 4, **то на втором этапе следует пропустить** 3, так как подмножество \[4, 3, ...\] полностью повторяет под- множество, созданное на этапе 1.
|
||
|
||
> В процессе поиска выбор на каждом уровне осуществляется слева направо.
|
||
>
|
||
> Поэтому чем правее ветвь, тем больше она обрезается.
|
||
|
||
1. На первых двух этапах выбираются 3 и 5 и создаются подмножества \[3, 5, ...\].
|
||
|
||
2. На первых двух этапах выбираются 4 и 5 и создаются подмножества \[4, 5, ...\].
|
||
|
||
3. Если на первом этапе выбирается 5, **то на втором этапе следует про- пустить** 3 **и** 4, так как подмножества \[5, 3, ...\] и \[5, 4, ...\] полностью повто- ряют подмножества, описанные на этапах 1 и 2.
|
||
|
||

|
||
|
||
> **Рис. 13.11.** Повторяющиеся подмножества, полученные в результате различного порядка выбора
|
||
>
|
||
> Обобщим эту мысль. Пусть задан входной массив \[*x*1, *x*2, ..., *xn*\]. Тогда в про- цессе поиска последовательность выбора \[*xi*1, *xi*2, ..., *xim*\] должна удовлетво- рять условию *i*1 ≤ *i*2 ≤ ⋯ ≤ *im*, **в противном случае она приведет к дубликатам, и ее нужно обрезать**.
|
||
|
||
##### Код реализации
|
||
|
||
> Для реализации этой обрезки мы инициализируем переменную start, которая указывает начальную точку обхода. **После выбора** *xi* **следующая итерация начинается с индекса** *i*. Это позволяет для последовательности выбора со- блюдать условие *i*1 ≤ *i*2 ≤ ⋯ ≤ *im*, обеспечивая уникальность подмножеств.
|
||
>
|
||
> Кроме того, в код были внесены следующие две оптимизации.
|
||
|
||
- Перед началом поиска массив nums сортируется. При обходе всех вариан- тов, если сумма подмножества превышает target, цикл завершается, так как последующие элементы больше, и их сумма также превысит target.
|
||
|
||
- Исключение переменной total, подсчет суммы элементов осуществля- ется с помощью вычитания из target. Решение фиксируется, когда target равен 0.
|
||
|
||
> \# === File: subset_sum_i.py ===
|
||
>
|
||
> def backtrack(
|
||
>
|
||
> state: list\[int\], target: int, choices: list\[int\], start: int, res: list\[list\[int\]\]
|
||
>
|
||
> ):
|
||
>
|
||
> \"\"\" Поиск с возвратом: сумма подмножеств I\"\"\"
|
||
>
|
||
> \# При равенстве суммы подмножества target фиксируется решение. if target == 0:
|
||
>
|
||
> res.append(list(state)) return
|
||
>
|
||
> \# Обход всех вариантов.
|
||
>
|
||
> \# Обрезка 2: обход начинается с start, чтобы избежать создания \# повторяющихся подмножеств.
|
||
>
|
||
> for i in range(start, len(choices)):
|
||
>
|
||
> \# Обрезка 1: если сумма подмножества превышает target, цикл завершается. \# Это связано с тем, что массив отсортирован, последующие элементы
|
||
>
|
||
> \# больше, и сумма подмножества обязательно превысит target. if target - choices\[i\] \< 0:
|
||
>
|
||
> break
|
||
>
|
||
> \# Попытка: выбор, обновление target, start. state.append(choices\[i\])
|
||
>
|
||
> \# Переход к следующему выбору.
|
||
>
|
||
> backtrack(state, target - choices\[i\], choices, i, res)
|
||
>
|
||
> \# Возврат: отмена выбора, восстановление предыдущего состояния. state.pop()
|
||
>
|
||
> def subset_sum_i(nums: list\[int\], target: int) -\> list\[list\[int\]\]: \"\"\" Решение задачи суммы подмножеств I.\"\"\"
|
||
>
|
||
> state = \[\] \# Состояние (подмножество). nums.sort() \# Сортировка nums.
|
||
>
|
||
> start = 0 \# Начальная точка обхода.
|
||
>
|
||
> res = \[\] \# Список результатов (список подмножеств). backtrack(state, target, nums, start, res)
|
||
>
|
||
> return res
|
||
>
|
||
> На рис. 13.12 показан полный процесс поиска с возвратом для массива \[3, 4, 5\] и целевого элемента 9.
|
||
>
|
||
> 
|
||
>
|
||
> **Рис. 13.12.** Процесс поиска с возвратом для реализации задачи о сумме подмножеств I
|
||
|
||
### Случай с повторяющимися элементами
|
||
|
||
> В отличие от предыдущей задачи **входной массив может содержать по- вторяющиеся элементы**, что создает новую проблему. Например, для мас- сива \[4, 4, 5\] и целевого элемента 9, текущий код выдает результат \[4, 5\], \[4, 5\], что приводит к повторяющимся подмножествам.
|
||
>
|
||
> **Причина этих повторов в том**, **что равные элементы выбираются не- сколько раз на одном этапе**. На рис. 13.13 показано, что на первом этапе есть три варианта выбора, два из которых равны 4. Это приводит к двум повторя- ющимся ветвям поиска и, следовательно, к повторяющимся подмножествам. Аналогично два элемента 4 на втором этапе также создают повторяющиеся подмножества.
|
||
>
|
||
> 
|
||
>
|
||
> **Рис. 13.13.** Повторяющиеся подмножества из-за равных элементов
|
||
|
||
##### Обрезка равных элементов
|
||
|
||
> Для решения этой проблемы **необходимо сделать выбор равных элементов на каждом этапе однократным**. Реализация этого подхода довольно изящ- на: поскольку массив отсортирован, равные элементы находятся рядом друг с другом. Это означает, что если текущий элемент равен предыдущему, то он уже был выбран, и его следует пропустить.
|
||
>
|
||
> В то же время **в этой задаче предусмотрено**, **что каждый элемент мас- сива может быть выбран только один раз**. К счастью, можно использовать переменную start для выполнения этого ограничения: после выбора *xi* начи- наем следующий цикл с индекса *i* + 1. Это позволяет исключить повторяющие- ся подмножества и избежать повторного выбора элементов.
|
||
|
||
##### Код реализации
|
||
|
||
> \# === File: subset_sum_ii.py === def backtrack(
|
||
>
|
||
> state: list\[int\], target: int, choices: list\[int\], start: int, res: list\[list\[int\]\]
|
||
>
|
||
> ):
|
||
>
|
||
> \"\"\" Поиск с возвратом: сумма подмножеств II.\"\"\"
|
||
>
|
||
> \# Когда сумма подмножества равна target, фиксируется решение. if target == 0:
|
||
>
|
||
> res.append(list(state)) return
|
||
>
|
||
> \# Перебор всех вариантов выбора.
|
||
>
|
||
> \# Обрезка 2: перебор начинается со start, чтобы избежать создания \# повторяющихся подмножеств.
|
||
>
|
||
> \# Обрезка 3: перебор начинается со start, чтобы избежать повторного выбора \# одного и того же элемента.
|
||
>
|
||
> for i in range(start, len(choices)):
|
||
>
|
||
> \# Обрезка 1: если сумма подмножества превышает target, цикл завершается. \# Это связано с тем, что массив уже отсортирован, и последующие
|
||
>
|
||
> \# элементы больше, сумма подмножества обязательно превысит target. if target - choices\[i\] \< 0:
|
||
>
|
||
> break
|
||
>
|
||
> \# Обрезка 4: если элемент равен левому элементу, значит, эта ветвь \# поиска повторяется, и ее можно пропустить.
|
||
>
|
||
> if i \> start and choices\[i\] == choices\[i - 1\]:
|
||
>
|
||
> continue
|
||
>
|
||
> \# Попытка: сделать выбор, обновить target, start. state.append(choices\[i\])
|
||
>
|
||
> \# Переход к следующему выбору.
|
||
>
|
||
> backtrack(state, target - choices\[i\], choices, i + 1, res)
|
||
>
|
||
> \# Возврат: отмена выбора, восстановление предыдущего состояния. state.pop()
|
||
>
|
||
> def subset_sum_ii(nums: list\[int\], target: int) -\> list\[list\[int\]\]: \"\"\" Решение задачи суммы подмножеств II.\"\"\"
|
||
>
|
||
> state = \[\] \# Состояние (подмножество).
|
||
>
|
||
> nums.sort() \# Сортировка nums.
|
||
>
|
||
> start = 0 \# Начальная точка перебора.
|
||
>
|
||
> res = \[\] \# Список результатов (список подмножеств). backtrack(state, target, nums, start, res)
|
||
>
|
||
> return res
|
||
>
|
||
> На рис. 13.14 демонстрируется процесс обратного отслеживания для масси- ва \[4, 4, 5\] и целевого элемента 9, включающий четыре вида обрезки. Проана- лизируйте рисунок и комментарии в коде, чтобы лучше понять весь процесс поиска и как работают различные операции обрезки.
|
||
>
|
||
> *1-й раунд выбора*
|
||
>
|
||
> *2-й раунд выбора*
|
||
>
|
||
> **Обрезка 4**
|
||
>
|
||
> *В одном раунде равные элементы можно выбирать только один раз*
|
||
>
|
||
> **Обрезка 2**
|
||
>
|
||
> *Не допускать создания повторяющихся подмножеств*
|
||
>
|
||
> **Обрезка 1**
|
||
>
|
||
> *Сумма элементов*
|
||
>
|
||
> *не может превышать*
|
||
>
|
||
> **target**
|
||
>
|
||
> **Рис. 13.14.** Процесс поиска с возвратом для реализации задачи о сумме подмножеств II
|
||
|
||
1. **задача об n ферзях**
|
||
|
||
> Для *n* = 4 можно найти два решения, которые изображены на рис. 13.15. С точки зрения алгоритма поиска с возвратом шахматная доска размером *n*×*n* имеет *n*2 клеток, которые предоставляют собой все варианты выбора. В про- цессе размещения ферзей состояние доски постоянно меняется, и в каждый момент времени доска имеет состояние state.
|
||
|
||
{width="3.312998687664042in" height="1.2706244531933508in"}
|
||
|
||
> **Рис. 13.15.** Решения задачи о 4 ферзях
|
||
>
|
||
> На рис. 13.16 изображено три условия ограничения для данной задачи: несколько ферзей не могут находиться на одной строке, в одном столбце или на одной диагонали. Стоит отметить, что диагонали делятся на главную диа- гональ \\ и побочную диагональ /.
|
||
|
||

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

|
||
|
||
> **Рис. 13.17.** Стратегия построчного размещения
|
||
>
|
||
> По сути**, стратегия построчного размещения выполняет функцию об- резки**, отсекая все ветви поиска, в которых на одной строке может находиться более одного ферзя.
|
||
|
||
##### Обрезка по столбцам и диагоналям
|
||
|
||
> Чтобы выполнить ограничениям по столбцам, можно использовать булев мас- сив cols длиной *n*, в котором будет фиксироваться наличие ферзя в каждом столбце. На его основе перед каждым решением о размещении будут обре- заться столбцы, в которых уже есть ферзь. Состояние cols будет динамически обновляться в процессе возврата.
|
||
>
|
||
> Как теперь отследить ограничения по диагоналям? Пусть индексы строки и столбца какой-либо клетки на доске равны (row, col). Выбрав определенную главную диагональ в матрице, можно заметить, что разность индексов строки и столбца всех клеток на этой диагонали одинакова, т. е. **для всех клеток на главной диагонали значение** row − col **является постоянной величиной**.
|
||
>
|
||
> Это означает, что если для двух клеток выполняется условие row1 -- col2 = row2 -- col2, то они находятся на одной главной диагонали. Пользуясь этим пра- вилом, можно с помощью массива diags1 фиксировать наличие ферзя на каж- дой главной диагонали, как показано на рис. 13.18.
|
||
>
|
||
> Аналогично **сумма** row + col **для всех клеток на побочной диагонали яв- ляется постоянной величиной**. Мы можем использовать еще один массив diags2 для обработки ограничений на побочной диагонали.
|
||
|
||

|
||
|
||
> **Рис. 13.18.** Обработка ограничений по столбцам и диагоналям
|
||
|
||
##### Код реализации
|
||
|
||
> Следует отметить, что в *n*-мерной матрице диапазон row -- col составляет \[−*n* + 1, *n* − 1\], а диапазон row + col составляет \[0, 2*n* − 2\], поэтому количество главных и побочных диагоналей равно 2*n* − 1, т. е. длина массивов diags1 и diags2 также равна 2*n* − 1.
|
||
>
|
||
> \# === File: n_queens.py === def backtrack(
|
||
>
|
||
> row: int, n: int,
|
||
>
|
||
> state: list\[list\[str\]\], res: list\[list\[list\[str\]\]\], cols: list\[bool\],
|
||
>
|
||
> diags1: list\[bool\], diags2: list\[bool\],
|
||
>
|
||
> ):
|
||
>
|
||
> \"\"\" Поиск с возвратом: задача об n ферзях.\"\"\"
|
||
>
|
||
> \# При размещении всех строк фиксируется решение. if row == n:
|
||
>
|
||
> res.append(\[list(row) for row in state\]) return
|
||
>
|
||
> \# Перебор всех столбцов. for col in range(n):
|
||
>
|
||
> \# Вычисление главной и побочной диагоналей для данной клетки. diag1 = row - col + n - 1
|
||
>
|
||
> diag2 = row + col
|
||
>
|
||
> \# Обрезка: не допускается наличие ферзя в данном столбце, на главной \# или побочной диагонали.
|
||
>
|
||
> if not cols\[col\] and not diags1\[diag1\] and not diags2\[diag2\]: \# Попытка: размещение ферзя в данной клетке. state\[row\]\[col\] = \"Q\"
|
||
>
|
||
> cols\[col\] = diags1\[diag1\] = diags2\[diag2\] = True \# Переход к следующей строке.
|
||
>
|
||
> backtrack(row + 1, n, state, res, cols, diags1, diags2) \# Возврат: восстановление клетки в пустое состояние. state\[row\]\[col\] = \"#\"
|
||
>
|
||
> cols\[col\] = diags1\[diag1\] = diags2\[diag2\] = False
|
||
>
|
||
> def n_queens(n: int) -\> list\[list\[list\[str\]\]\]: \"\"\" Решение задачи об n ферзях.\"\"\"
|
||
>
|
||
> \# Инициализация шахматной доски размером n\*n, где \'Q\' обозначает ферзя,
|
||
>
|
||
> \# а \'#\' обозначает пустую клетку.
|
||
>
|
||
> state = \[\[\"#\" for \_ in range(n)\] for \_ in range(n)\] cols = \[False\] \* n \# Учет наличия ферзя в столбце.
|
||
>
|
||
> diags1 = \[False\] \* (2 \* n - 1) \# Учет наличия ферзя на главной диагонали. diags2 = \[False\] \* (2 \* n - 1) \# Учет наличия ферзя на побочной диагонали. res = \[\]
|
||
>
|
||
> backtrack(0, n, state, res, cols, diags1, diags2) return res
|
||
>
|
||
> Размещение *n* раз по строкам с учетом ограничений по столбцам предпо- лагает, что от первой до последней строки имеется *n*, *n* − 1, \..., 2, 1 вариантов выбора, что требует времени *O*(*n*!). При фиксации решения необходимо копи- ровать матрицу state и добавлять результат в res, что требует времени *O*(*n*2). Таким образом, **общая временная сложность составляет** *O*(*n*! ⋅ *n*2). На прак- тике обрезка по ограничениям диагоналей значительно сокращает простран- ство поиска, поэтому эффективность поиска часто превосходит указанную временную сложность.
|
||
>
|
||
> Массив state использует *O*(*n*2) пространства, массивы cols, diags1 и diags2 ис- пользуют *O*(*n*) пространства. Максимальная глубина рекурсии составляет *n*, что требует *O*(*n*) пространства стека. Следовательно, **пространственная слож- ность равна** *O*(*n*2).
|
||
|
||
1. Резюме ❖ **395**
|
||
|
||
> **13.5. резюме**
|
||
|
||
##### Ключевые моменты
|
||
|
||
- Алгоритм поиска с возвратом по сути является методом полного пере- бора, который ищет подходящие решения путем обхода в глубину про- странства решений. В процессе поиска фиксируются удовлетворяющие условиям решения до тех пор, пока не будут найдены все решения или обход не будет завершен.
|
||
|
||
- Поиск с возвратом включает в себя попытки и возвраты. Он использует по- иск в глубину и выполняет попытки для различных вариантов. При несо- ответствии заданным условиям отменяет предыдущий выбор, возвраща- ется к предыдущему состоянию и продолжает проверять другие варианты. Попытки и возвраты -- это операции в противоположных направлениях.
|
||
|
||
- Задачи поиска с возвратом обычно содержат несколько ограничений, кото- рые можно использовать для обрезки. Обрезка позволяет заранее завершить ненужные ветви поиска, что значительно повышает эффективность поиска.
|
||
|
||
- Алгоритм поиска с возвратом в основном применяется для решения по- исковых задач и задач с ограничениями. Задачи комбинаторной опти- мизации можно решать с помощью поиска с возвратом, но часто суще- ствуют более эффективные или более подходящие методы.
|
||
|
||
- Задача о перестановках направлена на поиск всех возможных переста- новок элементов заданного множества. В решении используется массив для учета выбранных элементов и обрезки ветвей поиска с повторным выбором одного и того же элемента. Это позволяет обеспечить выбор каждого элемента только один раз.
|
||
|
||
- В задаче о перестановках с повторяющимися элементами нужно отсе- кать повторяющиеся перестановки в конечном результате. Необходимо обеспечить однократный выбор равных элементов в каждом раунде, что обычно реализуется с помощью хеш-множества.
|
||
|
||
- Цель задачи о сумме подмножеств -- найти все подмножества с суммой, равной целевому значению, в заданном множестве. Порядок элементов в множестве не важен, но процесс поиска выводит результаты во всех возможных порядках, создавая повторяющиеся подмножества. Перед выполнением поиска с возвратом данные сортируются, а также уста- навливается переменная для указания начальной точки каждого раунда, чтобы обрезать ветви поиска с повторяющимися подмножествами.
|
||
|
||
- В задаче о сумме подмножеств равные элементы в массиве создают по- вторяющиеся множества. При наличии предварительно отсортирован- ного массива обрезка осуществляется путем проверки равенства сосед- них элементов, что гарантирует выбор равных элементов только один раз в каждом раунде.
|
||
|
||
- Задача об *n* ферзях заключается в нахождении способа размещения *n* ферзей на шахматной доске размером *n*×*n* так, чтобы никакие два фер- зя не рубили друг друга. Ограничения задачи включают ограничения по строкам, столбцам, главным и побочным диагоналям. Для соблюдения ограничения по строкам используется стратегия размещения по стро- кам, что гарантирует размещение одного ферзя в каждой строке.
|
||
|
||
<!-- -->
|
||
|
||
- Обработка ограничений по столбцам и диагоналям осуществляется ана- логично. Для ограничения по столбцам используется массив, который фиксирует наличие ферзя в каждом столбце. Для ограничения по диаго- налям используются два массива, которые фиксируют наличие ферзя на главной и побочной диагоналях соответственно. Сложность заключается в нахождении закономерности индексов строк и столбцов для клеток, находящихся на одной и той же главной (или побочной) диагонали.
|
||
|
||
##### Вопросы и ответы
|
||
|
||
> **Вопрос**. Какова связь между возвратом и рекурсией?
|
||
>
|
||
> **Ответ**. В общем, возврат -- это стратегия алгоритма, тогда как рекурсия ско- рее является инструментом.
|
||
|
||
- Алгоритмы поиска с возвратом обычно реализуются на основе рекурсии. Однако поиск с возвратом -- это один из вариантов применения рекур- сии, а именно применение рекурсии в задачах поиска.
|
||
|
||
- Структура рекурсии отражает парадигму разбиения на подзадачи и ча- сто используется для решения задач, связанных со стратегией «разделяй и властвуй», поиском с возвратом, динамическим программированием (мемоизация рекурсии) и др.
|
||
|
||
> Глава 14
|