Во время недавнего гугления по какому-то поводу попалась мне на глаза заметка, в которой автор приводил реализацию алгоритма получения всех перестановок (permutations) на PHP. Видимо, по той причине, что обработка входного массива выполнялась "по месту", что существенно экономило память, реализация алгоритма получилась весьма витиеватой. То есть, чтобы разобраться, как он работает, пришлось натурально браться за ручку и бумагу ))
И вот задумался я, а можно ли как-то все это реализовать в более понятном ключе ?....
Ведь что такое множество перестановок для некоторого массива размерности N ? Алгоритм его получения можно условно представить таким псевдокодом:
Функция ВсеПерестановки(ВходнойМассив)
Результат = ПустойМассив();
Цикл по I от 1 до РазмерВходногоМассиваN
ПромежуточныйМассив = ВходнойМассив за вычетом элемента с индексом I;
ПромежуточныеПерестановки = ВсеПерестановки(ПромежуточныйМассив);
Цикл ПромежуточныеПерестановки как Перестановка
Перестановка = ЭлементВходногоМассива с индексом I + Перестановка;
КонецЦикла;
Результат = Результат + ПромежуточныеПерестановки;
КонецЦикла;
Вернуть Результат;
КонецФункции;
То есть, налицо рекурсивный алгоритм, где каждое решение для массива размерности N основывается на решении задачи для массива размерности N-1. Для того, чтобы получить решение для массива размерности N, нужно для каждого элемента этого массива выполнить следующие действия: получить промежуточный массив, который не содержит этого элемента
(размерность этого массива будет на 1 меньше), затем вызвать рекурсивно нашу функцию ВсеПерестановки() для этого промежуточного массива меньшей размерности, к полученным результатам функции "доклеить" в самое начало тот элемент, который мы удаляли из входного массива на первом шаге, а затем обработанный таким образом результат добавить для накопления в аккумулятор, значение которого будет возвращено по выходу из функции.
На мой взгляд, алгоритмически это решение весьма несложное для понимания. Попробуем реализовать этот алгоритм на PHP, применяя, по возможности, функциональный подход :)
Вот что получилось у меня:
===
function permutations($input) {
if (count($input) <= 1) return array($input);
$result = array();
foreach ($input as $v) {
$reduce = array_filter($input,
function ($e) use($v) { return $e !== $v; }
);
$result = array_merge( $result,
array_map(
function ($e) use ($v) { array_unshift( $e, $v); return $e; },
permutations( $reduce )
)
);
}
return $result;
}===
Вызов функции выглядит следующим образом:
$input = range(1, 5, 1);
$result = permutations($input);
Первый вызов - range() - возвращает нам массив, заполненный числам от 1 до 5 подряд.
Тут важный момент - все элементы массива должны быть различны, поэтому если вам нужно формировать перестановки для массива, в котором есть дубликаты, вы можете сформировать
массив индексов, как указано выше, получить перестановки для него, а потом использовать полученный результат для индексации оригинального массива.
Дальше этот массив поступает на вход функции, формирующей массив всех перестановок.
Количество перестановок будет равно факториалу от размерности входного массива.
Теперь давайте разберем, как работает permutations(). Она всегда возвращает массив массивов (даже для одного элемента). Это сделано специально, чтобы чуть упростить обработку результатов вложенного вызова самой себя. Итак, если на входе у нас пустой массив или массив, состоящий из одного элемента, мы возвращаем его же, но упакованным в массив.
То есть, на входе array(5), на выходе - array ( array (5) ).
Если же массив имеет более одного элемента, мы выполняем следующее: для каждого элемента $v массива мы формируем при помощи array_filter() промежуточный массив $reduce, который содержит все элементы входного массива за исключением этого элемента $v. После этого, мы вызываем permutations() для массива $reduce. Полученный результат обрабатываем при помощи array_map(), которая к каждому элементу массива перестановок добавляет в начало
элемент $v функцией array_unshift(). Далее, мы просто накапливаем полученные таким образом результаты в массиве $result, и после прохода по всему массиву $input возвращаем $result.
Все достаточно очевидно. Так как permutations() использует анонимные функции, то
требуется PHP версии не ниже 5.3 (это ограничение обходится написанием внешних функций, которые будут подаваться как callback в те же array_map() или array_filter().
Конечно, у такого подхода есть большой минус - он весьма "жруч" в плане памяти. Объясняется это, как минимум тем, что и array_map(), и array_merge(), и array_filter() в результате своей работы формируют новые массивы (функциональная иммутабельность! =)) Плюс еще и невозможность явно освобождать память в процессе работы. Я пытался вызывать внутри цикла unset(), но пользы это не принесло, к сожалению.
В общем, при попытке вызвать permutations() для входного массива, содержащего 10 элементов
(чуть более 3 с половиной миллионов перестановок), вываливается сообщение о нехватке ОЗУ:
==
Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 36 bytes)
==
Так что функцию можно применять либо как учебную, либо на массивах весьма ограниченной размерности.
Вот, пожалуй, и все на данный момент.
Дополнение.
Кстати, проверив работу функции, о которой я писал в самом начале этого поста, на размерности 10 элементов, я получил то же сообщение о нехватке памяти. То есть, проблема, в первую очередь, определяется не тем, что array_* функции иммутабельны и в PHP нет явного управления выделением и освобождением памяти, а тем, что все перестановки накапливаются в одном результирующем массиве, и уже этот массив целиком подается на выход функции permutations(). Решением проблемы может стать использование генераторов в PHP, которые появились в версии 5.5. То есть, вместо накопления результатов в массиве $result можно будет выдавать наверх каждую отдельную перестановку вызовом yield. По идее, это должно работать. Для более ранних версий PHP можно предусмотреть явный вызов из permutations() некоторой callback-функции с передачей в неё последней полученной перестановки как параметра. Это, конечно, будет не так изящно, как использование генератора, но тоже вполне приемлемо.
И вот задумался я, а можно ли как-то все это реализовать в более понятном ключе ?....
Ведь что такое множество перестановок для некоторого массива размерности N ? Алгоритм его получения можно условно представить таким псевдокодом:
Функция ВсеПерестановки(ВходнойМассив)
Результат = ПустойМассив();
Цикл по I от 1 до РазмерВходногоМассиваN
ПромежуточныйМассив = ВходнойМассив за вычетом элемента с индексом I;
ПромежуточныеПерестановки = ВсеПерестановки(ПромежуточныйМассив);
Цикл ПромежуточныеПерестановки как Перестановка
Перестановка = ЭлементВходногоМассива с индексом I + Перестановка;
КонецЦикла;
Результат = Результат + ПромежуточныеПерестановки;
КонецЦикла;
Вернуть Результат;
КонецФункции;
То есть, налицо рекурсивный алгоритм, где каждое решение для массива размерности N основывается на решении задачи для массива размерности N-1. Для того, чтобы получить решение для массива размерности N, нужно для каждого элемента этого массива выполнить следующие действия: получить промежуточный массив, который не содержит этого элемента
(размерность этого массива будет на 1 меньше), затем вызвать рекурсивно нашу функцию ВсеПерестановки() для этого промежуточного массива меньшей размерности, к полученным результатам функции "доклеить" в самое начало тот элемент, который мы удаляли из входного массива на первом шаге, а затем обработанный таким образом результат добавить для накопления в аккумулятор, значение которого будет возвращено по выходу из функции.
На мой взгляд, алгоритмически это решение весьма несложное для понимания. Попробуем реализовать этот алгоритм на PHP, применяя, по возможности, функциональный подход :)
Вот что получилось у меня:
===
function permutations($input) {
if (count($input) <= 1) return array($input);
$result = array();
foreach ($input as $v) {
$reduce = array_filter($input,
function ($e) use($v) { return $e !== $v; }
);
$result = array_merge( $result,
array_map(
function ($e) use ($v) { array_unshift( $e, $v); return $e; },
permutations( $reduce )
)
);
}
return $result;
}===
Вызов функции выглядит следующим образом:
$input = range(1, 5, 1);
$result = permutations($input);
Первый вызов - range() - возвращает нам массив, заполненный числам от 1 до 5 подряд.
Тут важный момент - все элементы массива должны быть различны, поэтому если вам нужно формировать перестановки для массива, в котором есть дубликаты, вы можете сформировать
массив индексов, как указано выше, получить перестановки для него, а потом использовать полученный результат для индексации оригинального массива.
Дальше этот массив поступает на вход функции, формирующей массив всех перестановок.
Количество перестановок будет равно факториалу от размерности входного массива.
Теперь давайте разберем, как работает permutations(). Она всегда возвращает массив массивов (даже для одного элемента). Это сделано специально, чтобы чуть упростить обработку результатов вложенного вызова самой себя. Итак, если на входе у нас пустой массив или массив, состоящий из одного элемента, мы возвращаем его же, но упакованным в массив.
То есть, на входе array(5), на выходе - array ( array (5) ).
Если же массив имеет более одного элемента, мы выполняем следующее: для каждого элемента $v массива мы формируем при помощи array_filter() промежуточный массив $reduce, который содержит все элементы входного массива за исключением этого элемента $v. После этого, мы вызываем permutations() для массива $reduce. Полученный результат обрабатываем при помощи array_map(), которая к каждому элементу массива перестановок добавляет в начало
элемент $v функцией array_unshift(). Далее, мы просто накапливаем полученные таким образом результаты в массиве $result, и после прохода по всему массиву $input возвращаем $result.
Все достаточно очевидно. Так как permutations() использует анонимные функции, то
требуется PHP версии не ниже 5.3 (это ограничение обходится написанием внешних функций, которые будут подаваться как callback в те же array_map() или array_filter().
Конечно, у такого подхода есть большой минус - он весьма "жруч" в плане памяти. Объясняется это, как минимум тем, что и array_map(), и array_merge(), и array_filter() в результате своей работы формируют новые массивы (функциональная иммутабельность! =)) Плюс еще и невозможность явно освобождать память в процессе работы. Я пытался вызывать внутри цикла unset(), но пользы это не принесло, к сожалению.
В общем, при попытке вызвать permutations() для входного массива, содержащего 10 элементов
(чуть более 3 с половиной миллионов перестановок), вываливается сообщение о нехватке ОЗУ:
==
Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 36 bytes)
==
Так что функцию можно применять либо как учебную, либо на массивах весьма ограниченной размерности.
Вот, пожалуй, и все на данный момент.
Дополнение.
Кстати, проверив работу функции, о которой я писал в самом начале этого поста, на размерности 10 элементов, я получил то же сообщение о нехватке памяти. То есть, проблема, в первую очередь, определяется не тем, что array_* функции иммутабельны и в PHP нет явного управления выделением и освобождением памяти, а тем, что все перестановки накапливаются в одном результирующем массиве, и уже этот массив целиком подается на выход функции permutations(). Решением проблемы может стать использование генераторов в PHP, которые появились в версии 5.5. То есть, вместо накопления результатов в массиве $result можно будет выдавать наверх каждую отдельную перестановку вызовом yield. По идее, это должно работать. Для более ранних версий PHP можно предусмотреть явный вызов из permutations() некоторой callback-функции с передачей в неё последней полученной перестановки как параметра. Это, конечно, будет не так изящно, как использование генератора, но тоже вполне приемлемо.
Хорошая статья. Спасибо. Для себя недавно понял, что псевдокод на русском языке часто играет ключевую роль для понимания, чем долгие объяснения на пальцах или готовый программный код.
ОтветитьУдалитьСпасибо за отзыв ) Рад, что мои заметки читают люди (хоть и редко =)
ОтветитьУдалитьПсевдокод действительно часто облегчает жизнь при объяснении логики работы алгоритмов.
Я очень долго был в медитации перед разными реализациями его этого алгоритма, честно скажу, я не мог понять :)
ОтветитьУдалитьЛуч света появился только, когда увидел ваш псевдокод на русском, сегодня еще раз перечитал и понял суть.
Однако придумал еще такой вариант, фактически, без рекурсии. Идея заключается в том, чтобы сначала сгенерировать все размещения, например в файл (да, это долго, работает до 7 эл включительно :)), а потом вынуть те, строки, в которых символ повторяется более одного раза, пройтись по всем входным символам.
Вот, набросал статейку:
http://forum.php.su/topic.php?forum=79&topic=5421&v=l#1410012311
Ну, тут все дело в требовании к ресурсам (производительности и расходу памяти). В коде из статьи основная проблема - это высокие требования к памяти. Но её можно обойти, если использовать PHP-генераторы, например.
ОтветитьУдалитьВ этом случае не нужно будет накапливать данные в массив. Рекурсия просто делает легким понимание алгоритма. А вообще, конечно, PHP - не самый лучший язык для такого рода вычислений, как мне кажется.
Статью по вашей ссылке прочитал. В принципе, можно и так - исключением из полного списка всех возможных комбинаций тех, где есть дубликаты элементов. Просто это добавляет дополнительных вычислений.
Кстати, когда формируется первый файл с полным списком все вариантов, то почему цикл идет до 3124 ? Если у нас 4 позиции по 4 возможных значения в каждой, то всего должно быть 4^4 = 256 значений ?
Или я где-то неправильно считаю ?
Да, я там ошибся. Делал сначала для 5^5. Да и инкремент там сатоял постфиксный.
ОтветитьУдалитьВот еще нашел такой вариант в связи с другой задачей:
http://phpclub.ru/talk/threads/%D0%90%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC-%D0%BD%D0%B0%D1%85%D0%BE%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D1%8F-%D0%BF%D0%B5%D1%80%D0%B5%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BE%D0%BA.12483/
В принципе нерекурсивный получился у человека алгортим, эксплуатируется поиск по массиву (in_array).
Очевидная польза рекурсивного алгоритма - его легче понять (в частности, решение, приведенное по последней ссылке, не так очевидно, на мой взгляд). Хотя, конечно, рекурсивные алгоритмы, как правило, более затратны по времени и ресурсам.
ОтветитьУдалитьСоглсен, я даже скажу, что пока не понимаю (пошагово), как работает алгоритм по ссылке, но я не знаю всех средств php. И тот код на 9 элементах выбрасывает can't allocate memory.
ОтветитьУдалитьВот еще мой ночной вариант, все недочеты описаны, там можно много выкинуть: http://habrahabr.ru/post/260111/
Однако даже с грязными приемами 9 чисел я сгенерировал, самый верхний цикл убрал для наглядности. Сначала вывел на бумажке его, потом уже прочитал, что алгоритм Троттера использует ту же идею. В принципе, если его оптимизировать, то операций там немного.
Сейчас вывел еще такое на бумажке, на бумажке работает:
https://toster.ru/q/224635
Но я не уверен, поэтому задал вопрос.
C уважением, dcc0
Прочитал статью на хабре. В принципе, это и есть объяснение работы рекурсивного алгоритма, когда решение задачи размерности N получается через использование решения размерности N-1.
ОтветитьУдалитьЧто касается вопроса на тостере, то самый надеждый способ проверить работоспособность алгоритма - это написать программку, реализующую псевдокод, и уже ее прогнать на тестовых данных. Для сортировки части массива можно сделать так: выделить эту часть в отдельный массив, его уже отсортировать и затем после сортировки заменить старый фрагмент отсортированным.
Да, все же собрал полный алгоритм на PHP без рекурсии и в лексикографическом порядке. 9 цифр берет. Но операций довольно много.
Удалитьhttp://habrahabr.ru/post/260997/
Спасибо, попробую.
ОтветитьУдалитьПробовал так: array_slice до нужного числа, затем в обратном порядке array_unshift (но получается, что надо в цикле), т.е. все опять усложняется.
dcc0
Для вставки отcортированного массива обратно посмотрите в сторону функции array_splice:
ОтветитьУдалитьhttp://php.net/manual/ru/function.array-splice.php
Нашел на bash медленный, но интересный код перестановок в лексикографическом порядке, код алгоритм рекурсивный, но очень компактный.
ОтветитьУдалитьhttp://stackoverflow.com/questions/3846123/generating-permutations-using-bash
Правда, до конца еще его не понял.
perm "${items:0:i}${items:i+1}" "$out${items:i:1}" )
Как понимаю, - функция в цикле меняет позиции отображения для "субаргументов" основного аргумента, если так можно выразиться :), т.е. если первый, т.е. если один обрзается, то один другой увеличивается.
Но непонятно вот это "$out${items:i:1}".
Разобрать бы его подробно на примере
dcc0
Да, красивое решение. Я практически не знаю баш, но чисто интуитивно переписал алгоритм на PHP:
ОтветитьУдалить====
function perm($input, $output = '') {
if(!$input) {
echo $output . "<br>";
return;
}
for($i=0; $i < strlen($input); ++$i) {
$newInput = substr_replace($input, '', $i, 1);
$newOutput = $output . $input{$i};
perm($newInput, $newOutput);
}
}
perm('abcd');
===
"$out${items:i:1}" - это, по сути, склейка двух переменных в строку: $out - то, что функция perm() получила вторым параметром, а ${items:i:1} - это один символ (текущий) из входной строки $items по индексу i. Если я правильно понимаю.
Первым делом подумал про substr. Правда, функция переставляет символы по одному, т.е. можно больше 9 символов, а вот с цифрами уже сложнее, так как если брать 10 это уже два символа, но решение действительно красивое.
ОтветитьУдалитьСпасибо за объяснение.
dcc0
Понял, кажется, на вашем примере. Если правильно понял:
ОтветитьУдалитьПри первом запуске вывода нет, так как условие. Так ли?
$newInput = substr_replace($input, '', $i, 1);
Каждый раз заменяется одна позиция в input, т.е. символ вырезается. Для 123 получается смещение. Вырезали первый, 23, второй 13, третий 12, остаётся только дописать символ в нужную позицию.
Да. Если представить себе процесс работы этого алгоритма в виде дерева, то операция вывода (echo) будет выполняться для самых нижних узлов (листьев).
ОтветитьУдалитьСпасибо еще раз.
ОтветитьУдалитьВот странно, нигде пока не нашел реализации инверсионного способа нахождения перестановок. Метод интересный, хоть и довольно затяжной.
dcc0
Возможно, это как-то связано с тем, что в этом случае придется дополнительно генерировать таблицу инверсий ? Сложно сказать.
ОтветитьУдалитьДа, вероятно ... вроде бы есть способ генерации таблицы инверсий с помощью код Грея. Только все равно непонятно, в учебниках по алгоритмам пишут, что для каждого разряда надо использовать свою систему счисления. Странно, что нет даже учебных реализаций.
ОтветитьУдалитьВот еще один способ генерации нашел, наиболее прозрачен на мой взгляд.
И, кстати, субъективно наглядность лучше проступает там, где код написан с применением минимального количества "встроенных функций" функций.
http://forum.ru-board.com/topic.cgi?forum=31&topic=16348
dcc0
Посмотрел пост по ссылке. Да, тоже интересный вариант. Там, как я понял, в каждый вызов BackTrace() передается массив масок, в котором уже отмечены используемые на данный момент элементы.
ОтветитьУдалить