#list #haskell #permutation #combinatorics
#Список #haskell #перестановка #комбинаторика
Вопрос:
Я изучаю код в данных.Модуль списка и я не могу точно понять эту реализацию перестановок:
permutations :: [a] -> [[a]]
permutations xs0 = xs0 : perms xs0 []
where
perms [] _ = []
perms (t:ts) is = foldr interleave (perms ts (t:is)) (permutations is)
where interleave xs r = let (_,zs) = interleave' id xs r in zs
interleave' _ [] r = (ts, r)
interleave' f (y:ys) r = let (us,zs) = interleave' (f . (y:)) ys r
in (y:us, f (t:y:us) : zs)
Может кто-нибудь подробно объяснить, как эти вложенные функции соединяются / работают друг с другом?
Комментарии:
1. Могли бы задать некоторые конкретные вопросы, которые у вас есть по этому поводу? Какие части вы понимаете?
2. Ну, в основном я не могу понять, что делают perms и interleave / interleave. Существует ли какой-либо конкретный алгоритм, на котором основана эта реализация?
3. Причина, по которой эта функция настолько сложна, заключается в том, что она очень старается быть максимально ленивой. Например, вы можете запустить
map (take 3) $ permutations (1:2:3:undefined)
и получить[[1,2,3],[2,1,3],[3,2,1],[2,3,1],[3,1,2],[1,3,2]*** Exception: Prelude.undefined
; но не получить undefined раньше. Обсуждение списка рассылки должно дать некоторое представление о его дизайне, haskell.1045720.n5.nabble.com /…4. @TwanvanLaarhoven, не могли бы вы объяснить, что
interleave
предполагается делать? Это немного непрозрачно.5. Прошло много времени с тех пор, как я написал этот код, так что мне придется снова разобраться с этим самому. Я помню, что
f
параметр — это функция, которая отображается поверх конечного результата. Я сделаю более подробный отчет позже сегодня.
Ответ №1:
Извините за поздний ответ, запись заняла немного больше времени, чем ожидалось.
Итак, прежде всего, чтобы максимизировать леность в функции списка, подобной этой, есть две цели:
- Создайте как можно больше ответов, прежде чем проверять следующий элемент входного списка
- Сами ответы должны быть ленивыми, и поэтому там должно быть то же самое.
Теперь рассмотрим permutation
функцию. Здесь максимальная ленивость означает:
- Мы должны определить, что существуют по крайней мере
n!
перестановки после проверки толькоn
элементов ввода - Для каждой из этих
n!
перестановок первыеn
элементы должны зависеть только от первыхn
элементов ввода.
Первое условие может быть формализовано как
length (take (factorial n) $ permutations ([1..n] undefined))) `seq` () == ()
Дэвид Бенбенник формализовал второе условие как
map (take n) (take (factorial n) $ permutations [1..]) == permutations [1..n]
В совокупности мы имеем
map (take n) (take (factorial n) $ permutations ([1..n] undefined)) == permutations [1..n]
Давайте начнем с нескольких простых случаев. Первый permutation [1..]
. Мы должны иметь
permutations [1..] = [1,???] : ???
И с двумя элементами мы должны иметь
permutations [1..] = [1,2,???] : [2,1,???] : ???
Обратите внимание, что нет выбора порядка первых двух элементов, мы не можем поставить [2,1,...]
их на первое место, поскольку мы уже решили, что первая перестановка должна начинаться с 1
. К настоящему времени должно быть ясно, что первый элемент permutations xs
должен быть равен самому xs
себе.
Теперь перейдем к реализации.
Прежде всего, есть два разных способа выполнить все перестановки списка:
-
Selection style: keep picking elements from the list until there are none left
permutations [] = [[]] permutations xxs = [(y:ys) | (y,xs) <- picks xxs, ys <- permutations xs] where picks (x:xs) = (x,xs) : [(y,x:ys) | (y,ys) <- picks xs]
-
Insertion style: insert or interleave each element in all possible places
permutations [] = [[]] permutations (x:xs) = [y | p <- permutations xs, y <- interleave p] where interleave [] = [[x]] interleave (y:ys) = (x:y:ys) : map (y:) (interleave ys)
Note that neither of these is maximally lazy. The first case, the first thing this function does is pick the first element from the entire list, which is not lazy at all. In the second case we need the permutations of the tail before we can make any permutation.
To start, note that interleave
can be made more lazy. The first element of interleave yss
list is [x]
if yss=[]
or (x:y:ys)
if yss=y:ys
. But both of these are the same as x:yss
, so we can write
interleave yss = (x:yss) : interleave' yss
interleave' [] = []
interleave' (y:ys) = map (y:) (interleave ys)
Реализация в данных.Список продолжает эту идею, но использует еще несколько приемов.
Возможно, проще всего перейти к обсуждению списка рассылки. Мы начнем с версии Дэвида Бенбенника, которая совпадает с той, которую я написал выше (без ленивого чередования). Мы уже знаем, что первым элементом permutations xs
должен быть xs
он сам. Итак, давайте вставим это в
permutations xxs = xxs : permutations' xxs
permutations' [] = []
permutations' (x:xs) = tail $ concatMap interleave $ permutations xs
where interleave = ..
Вызов tail
, конечно, не очень приятный. Но если мы встроим определения permutations
и interleave
получим
permutations' (x:xs)
= tail $ concatMap interleave $ permutations xs
= tail $ interleave xs concatMap interleave (permutations' xs)
= tail $ (x:xs) : interleave' xs concatMap interleave (permutations' xs)
= interleave' xs concatMap interleave (permutations' xs)
Теперь у нас есть
permutations xxs = xxs : permutations' xxs
permutations' [] = []
permutations' (x:xs) = interleave' xs concatMap interleave (permutations' xs)
where
interleave yss = (x:yss) : interleave' yss
interleave' [] = []
interleave' (y:ys) = map (y:) (interleave ys)
Следующий шаг — оптимизация. Важной целью было бы исключить вызовы ( ) в чередовании. Это не так просто из-за последней строки, map (y:) (interleave ys)
. Мы не можем сразу использовать трюк foldr / ShowS для передачи хвоста в качестве параметра. Выход — избавиться от map. Если мы передадим параметр f
в качестве функции, которая должна быть отображена поверх результата в конце, мы получим
permutations' (x:xs) = interleave' id xs concatMap (interleave id) (permutations' xs)
where
interleave f yss = f (x:yss) : interleave' f yss
interleave' f [] = []
interleave' f (y:ys) = interleave (f . (y:)) ys
Теперь мы можем перейти в хвост,
permutations' (x:xs) = interleave' id xs $ foldr (interleave id) [] (permutations' xs)
where
interleave f yss r = f (x:yss) : interleave' f yss r
interleave' f [] r = r
interleave' f (y:ys) r = interleave (f . (y:)) ys r
Это начинает выглядеть как в Data.List, но это еще не то же самое. В частности, это не так лениво, как могло бы быть.
Давайте попробуем:
*Main> let n = 4
*Main> map (take n) (take (factorial n) $ permutations ([1..n] undefined))
[[1,2,3,4],[2,1,3,4],[2,3,1,4],[2,3,4,1]*** Exception: Prelude.undefined
О, только первые n
элементы правильные, а не первые factorial n
.
Причина в том, что мы все еще пытаемся разместить первый элемент ( 1
в приведенном выше примере) во всех возможных местах, прежде чем пытаться что-либо еще.
Ицхак Гейл придумал решение. Рассмотрены все способы разделения входных данных на начальную часть, средний элемент и хвост:
[1..n] == [] 1 : [2..n]
== [1] 2 : [3..n]
== [1,2] 3 : [4..n]
Если вы раньше не видели трюк для их создания, вы можете сделать это с zip (inits xs) (tails xs)
помощью .
Теперь перестановки [1..n]
будут
[] 1 : [2..n]
он же.[1..n]
, или2
вставляется (чередуется) где-то в перестановку[1]
, за которой следует[3..n]
. Но не2
вставляется в конце[1]
, поскольку мы уже переходим к этому результату в предыдущем пункте.3
чередуется с перестановкой[1,2]
(не в конце), за которой следует[4..n]
.- и т.д.
Вы можете видеть, что это максимально лениво, поскольку, прежде чем мы даже подумаем о том, чтобы что-то сделать 3
, мы дали все перестановки, которые начинаются с некоторой перестановки [1,2]
. Код, который дал Ицхак, был
permutations xs = xs : concat (zipWith newPerms (init $ tail $ tails xs)
(init $ tail $ inits xs))
where
newPerms (t:ts) = map ( ts) . concatMap (interleave t) . permutations3
interleave t [y] = [[t, y]]
interleave t ys@(y:ys') = (t:ys) : map (y:) (interleave t ys')
Обратите внимание на рекурсивный вызов permutations3
, который может быть вариантом, который не должен быть максимально ленивым.
Как вы можете видеть, это немного менее оптимизировано, чем то, что у нас было раньше. Но мы можем применить некоторые из тех же приемов.
Первый шаг — избавиться от init
and tail
. Давайте посмотрим, что на zip (init $ tail $ tails xs) (init $ tail $ inits xs)
самом деле
*Main> let xs = [1..5] in zip (init $ tail $ tails xs) (init $ tail $ inits xs)
[([2,3,4,5],[1]),([3,4,5],[1,2]),([4,5],[1,2,3]),([5],[1,2,3,4])]
init
Избавляется от комбинации ([],[1..n])
, в то время tail
как избавляется от комбинации ([1..n],[])
. Нам не нужно первое, потому что это приведет к сбою сопоставления с шаблоном newPerms
. Последнее завершится неудачей interleave
. Оба легко исправить: просто добавьте регистр для newPerms []
и для interleave t []
.
permutations xs = xs : concat (zipWith newPerms (tails xs) (inits xs))
where
newPerms [] is = []
newPerms (t:ts) is = map ( ts) (concatMap (interleave t) (permutations is))
interleave t [] = []
interleave t ys@(y:ys') = (t:ys) : map (y:) (interleave t ys')
Теперь мы можем попробовать встроить tails
и inits
. Их определение
tails xxs = xxs : case xxs of
[] -> []
(_:xs) -> tails xs
inits xxs = [] : case xxs of
[] -> []
(x:xs) -> map (x:) (inits xs)
Проблема в том, что inits
это не хвостовая рекурсия. Но поскольку мы все равно собираемся выполнить перестановку инициализаций, порядок элементов нас не волнует. Итак, мы можем использовать накапливающийся параметр,
inits' = inits'' []
where
inits'' is xxs = is : case xxs of
[] -> []
(x:xs) -> inits'' (x:is) xs
Now we make newPerms
a function of xxs
and this accumulating parameter, instead of tails xxs
and inits xxs
.
permutations xs = xs : concat (newPerms' xs [])
where
newPerms' xxs is =
newPerms xxs is :
case xxs of
[] -> []
(x:xs) -> newPerms' xs (x:is)
newPerms [] is = []
newPerms (t:ts) is = map ( ts) (concatMap (interleave t) (permutations3 is))
inlining newPerms
into newPerms'
then gives
permutations xs = xs : concat (newPerms' xs [])
where
newPerms' [] is = [] : []
newPerms' (t:ts) is =
map ( ts) (concatMap (interleave t) (permutations is)) :
newPerms' ts (t:is)
встраивание и развертывание concat
, а также перемещение финала map ( ts)
в interleave
,
permutations xs = xs : newPerms' xs []
where
newPerms' [] is = []
newPerms' (t:ts) is =
concatMap interleave (permutations is)
newPerms' ts (t:is)
where
interleave [] = []
interleave (y:ys) = (t:y:ys ts) : map (y:) (interleave ys)
Затем, наконец, мы можем повторно foldr
применить трюк, чтобы избавиться от ( )
:
permutations xs = xs : newPerms' xs []
where
newPerms' [] is = []
newPerms' (t:ts) is =
foldr (interleave id) (newPerms' ts (t:is)) (permutations is)
where
interleave f [] r = r
interleave f (y:ys) r = f (t:y:ys ts) : interleave (f . (y:)) ys r
Подождите, я сказал избавиться от ( )
. Мы избавились от одного из них, но не от того, в interleave
.
Для этого мы можем видеть, что мы всегда объединяем некоторый хвост yys
to ts
. Итак, мы можем развернуть вычисления (ys ts)
вместе с рекурсией interleave
, и функция interleave' f ys r
вернет кортеж (ys ts, interleave f ys r)
. Это дает
permutations xs = xs : newPerms' xs []
where
newPerms' [] is = []
newPerms' (t:ts) is =
foldr interleave (newPerms' ts (t:is)) (permutations is)
where
interleave ys r = let (_,zs) = interleave' id ys r in zs
interleave' f [] r = (ts,r)
interleave' f (y:ys) r =
let (us,zs) = interleave' (f . (y:)) ys r
in (y:us, f (t:y:us) : zs)
И вот оно, Data.List.permutations
во всей его максимально ленивой оптимизированной славе.
Отличная статья от Twan! Я (@Yitz) просто добавлю несколько ссылок:
-
Оригинальная ветка электронной почты, в которой Twan разработал этот алгоритм, ссылка на которую приведена выше Twan, является увлекательным чтением.
-
Кнут классифицирует все возможные алгоритмы, которые удовлетворяют этим критериям в Vol. 4 Fasc. 2 Раздел 7.2.1.2.
-
Twan
permutations3
по сути такой же, как «Алгоритм P» Кнута. Насколько известно Кнуту, этот алгоритм был впервые опубликован английскими церковными звонарями в 1600-х годах.
Комментарии:
1. Разве ваша
interleave
функция (по крайней мере, первые) не пропускаетx
в своем списке параметров?2. Я не совсем понимаю, что вы подразумеваете под вставкой и выбором.
3. @Bergi: поскольку
interleave
функция объявлена вwhere
блоке, переменныеx
иxs
находятся в области видимости, поскольку они являются аргументами внешней функции,permutations
.4. @dfeuer: для выполнения перестановок
[1,2,3]
есть две стратегии: * возьмите перестановки[2,3]
, которые есть[[2,3],[3,2]]
, и рассмотрите все места для вставки1
в эти списки. * рассмотрите все возможные начала перестановки, которые являются1
,2
или3
(выберите элемент из входных данных), и продолжайте с перестановкой оставшихся элементов.5. @TwanvanLaarhoven: Да, я видел это (даже если это выглядело немного нетрадиционно), но, по крайней мере, в первом примере была ошибка. Пожалуйста, проверьте мое редактирование 🙂
Ответ №2:
Базовый алгоритм основан на идее брать по одному элементу из списка за раз, находить каждую перестановку элементов, включая эту новую, а затем повторять.
Чтобы объяснить, как это выглядит, [1 ..] будет означать список от единицы вверх, где никакие значения (даже первые) еще не были рассмотрены. Это параметр функции. Результирующий список выглядит примерно так:
[[1..]]
[[2,1,3..]]
[[3,2,1,4..], [2,3,1,4..]] [[3,1,2,4..], [1,3,2,4..]]
[[4,3,2,1,5..], etc
Приведенная выше кластеризация отражает основную идею алгоритма … каждая строка представляет новый элемент, взятый из входного списка и добавленный к набору элементов, которые переставляются. Кроме того, она рекурсивна … в каждой новой строке она принимает все существующие перестановки и помещает элемент в каждое место, где он еще не был (все места, кроме последнего). Итак, в третьей строке у нас есть две перестановки [2,1] и [1,2], а затем мы занимаем 3 места в обоих доступных слотах, так что [[3,2,1], [2,3,1]] и [[3,1,2], [1,3,2]] соответственно, а затем добавляем любую ненаблюдаемую частьесть.
Надеюсь, это хотя бы немного прояснит алгоритм. Однако необходимо объяснить некоторые оптимизации и детали реализации.
(Примечание: используются две основные оптимизации производительности: во-первых, если вы хотите повторно добавлять некоторые элементы в несколько списков, map (x:y:z:) list
это намного быстрее, чем сопоставление с некоторыми условными или шаблонными сопоставлениями, потому что у него нет ветвления, просто вычисляемый переход. Во-вторых, и это часто используется, дешево (и удобно) создавать списки от конца к началу, многократно добавляя элементы; это используется в нескольких местах.
Первое, что делает функция, это устанавливает два базовых случая: во-первых, каждый список имеет по крайней мере одну перестановку: себя. Это может быть возвращено без какой-либо оценки. Это можно рассматривать как случай «взять 0».
Внешний цикл — это часть, которая выглядит следующим образом:
perms (t:ts) is = <prepend_stuff_to> (perms ts (t:is))
ts
это «нетронутая» часть списка, которую мы еще не переставляли и даже еще не изучали, и изначально представляет собой всю входную последовательность.
t
это новый элемент, который мы будем вставлять между перестановками.
is
это список элементов, которые мы будем переставлять, а затем помещать t
между ними, и изначально он пуст.
Каждый раз, когда мы вычисляем одну из приведенных выше строк, мы доходим до конца элементов, которые мы добавили к блоку, содержащему (perms ts (t:is)) и повторяем.
Второй цикл — это foldr. Это для каждой перестановки is
(материала перед текущим элементом в исходном списке), это interleave
элемент в этот список и добавляет его к thunk.
foldr interleave <thunk> (permutations is)
Третий цикл является одним из самых сложных. Мы знаем, что она добавляет каждое возможное вкрапление нашего целевого элемента t
в перестановку, за которой следует ненаблюдаемый хвост в результирующую последовательность. Он делает это с помощью рекурсивного вызова, где он складывает перестановку в стек функций по мере рекурсии, а затем, когда он возвращается, он выполняет то, что составляет два небольших конечных автомата для построения результатов.
Давайте посмотрим на пример: interleave [<thunk>] [1,2,3]
где t = 4
и is = [5..]
Во-первых, поскольку interleave’ вызывается рекурсивно, он создает y
s и f
s в стеке, например:
y = 1, f = id
y = 2, f = (id . (1:))
y = 3, f = ((id . (1:)) . (2:))
(the functions are conceptually the same as ([] ), ([1] ), and ([1,2] ) respectively)
Затем, когда мы возвращаемся назад, мы возвращаем и оцениваем кортеж, содержащий два значения, (us, zs)
.
us
это список, к которому мы добавляем y
s после нашей цели t
.
zs
это накопитель результатов, где каждый раз, когда мы получаем новую перестановку, мы добавляем ее в списки результатов.
Таким образом, чтобы закончить пример, f (t:y:us)
вычисляется и возвращается в качестве результата для каждого уровня стека выше.
([1,2] ) (4:3:[5..]) === [1,2,4,3,5..]
([1] ) (4:2[3,5..]) === [1,4,2,3,5..]
([] ) (4:1[2,3,5..]) === [4,1,2,3,5..]
Надеюсь, это поможет или, по крайней мере, дополнит материал, связанный в комментарии автора выше.
(Спасибо dfeuer за то, что он поднял этот вопрос в IRC и обсуждал его в течение нескольких часов)