* docs(ru): replace prose quotes with guillemets * docs(ru): replace prose semicolons with periods * docs(ru): align animation title forms * docs(ru): align figure and table references
10 KiB
Задача о перестановках
Задача о перестановках является типичным применением алгоритма поиска с возвратом. Ее определение состоит в том, чтобы для данного множества элементов (например, массива или строки) найти все возможные перестановки этих элементов.
В таблице ниже приведено несколько примеров входных массивов и соответствующих им перестановок.
Таблица Примеры перестановок
| Входной массив | Все перестановки |
|---|---|
[1] |
[1] |
[1, 2] |
[1, 2], [2, 1] |
[1, 2, 3] |
[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1] |
Случай без равных элементов
!!! question
Дан массив целых чисел, в котором нет повторяющихся элементов. Верните все возможные перестановки.
С точки зрения поиска с возвратом процесс построения перестановок можно представить как результат последовательности выборов. Пусть входной массив равен [1, 2, 3]. Если мы сначала выберем 1 , затем 3 , а потом 2 , то получим перестановку [1, 3, 2] . Откат здесь означает отмену одного из выборов с последующей попыткой других вариантов.
С точки зрения кода поиска с возвратом множество кандидатов choices состоит из всех элементов входного массива, а состояние state - из элементов, уже выбранных к текущему моменту. Поскольку каждый элемент разрешено выбирать только один раз, все элементы в state должны быть уникальны.
Как показано на рисунке ниже, процесс поиска можно развернуть в дерево рекурсии, где каждый узел представляет текущее состояние state . Начиная от корня, после трех раундов выбора мы попадаем в листья, и каждый лист соответствует одной перестановке.
Обрезка повторного выбора
Чтобы гарантировать, что каждый элемент выбирается только один раз, введем булев массив selected , где selected[i] обозначает, был ли уже выбран choices[i] , и на его основе выполним следующую обрезку.
- После того как сделан выбор
choice[i], мы присваиваемselected[i]значение\text{True}, тем самым отмечая, что этот элемент уже выбран. - При обходе списка вариантов
choicesпропускаем все уже выбранные элементы, то есть выполняем обрезку.
Как показано на рисунке ниже, если в первом раунде мы выберем 1 , во втором - 3 , а в третьем - 2 , то во втором раунде нужно отсечь ветвь элемента 1 , а в третьем - ветви элементов 1 и 3 .
Как видно на рисунке выше, такая обрезка уменьшает размер пространства поиска с O(n^n) до O(n!) .
Реализация кода
После прояснения всей логики можно просто «заполнить пропуски» в шаблоне поиска с возвратом. Чтобы сократить общий объем кода, мы не будем отдельно реализовывать каждую функцию из каркаса, а раскроем их прямо внутри backtrack() :
[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 , которое будет записывать элементы, уже встречавшиеся в этом раунде, и отсекать повторы:
[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цикле), и нужен для того, чтобы равные элементы выбирались только один раз.
На рисунке ниже показана область действия двух условий обрезки. Помните, что каждый узел дерева соответствует одному выбору, а путь от корня до листа образует одну перестановку.