Бесконечный список ссылок на самих себя

Проблема

Я пытаюсь реализовать модифицированную Кривую дракона из AoC Day 16 в виде бесконечного списка в Haskell.

Список состоит из True и False. Начнем с некоторого списка s0:

  • s1 = s0 ++ [False] ++ (map not . reverse) s0
  • s2 = s1 ++ [False] ++ (map not . reverse) s1
  • s3 = s2 ++ [False] ++ (map not . reverse) s2

В целом

sn = s(n-1) ++ [0] ++ (map not . reverse) s(n-1) 
   = s0 ++ [0] ++ (f s0) ++ [0] ++ (f (s0 ++ [0] ++ (f s0))) ++ ...
      where f = (map not . reverse)

Попытка реализации

Я могу легко получить sn, используя функцию iterate.

modifiedDragonCurve :: [Bool] -> Int -> [Bool]
modifiedDragonCurve s n = (iterate f s)!!n
    where f s     = s ++ [False] ++ (map not . reverse) s 

Это дает мне список [s0, s1, s2, ...]. Однако, поскольку s(n-1) является префиксом sn, его можно построить как бесконечный список, но я не могу понять, как к нему подойти. Я думаю, мне нужно что-то вроде

modifiedDragonCurve :: [Bool] -> [Bool]
modifiedDragonCurve s = s ++ [False] ++ (map not . reverse) listSoFar

Но не могу понять, как обратиться к уже сгенерированному списку (listSoFar).

Любые предложения будут ценны.


person Bart Platak    schedule 16.12.2016    source источник
comment
Что именно вы хотите получить?   -  person freestyle    schedule 16.12.2016
comment
@freestyle: я хочу создать функцию [Bool] -> [Bool], которая (данный начальный список) генерирует бесконечный список. По сути, я хочу реализовать последовательность sn = s(n-1) ++ [0] ++ (map not . reverse) s(n-1)   -  person Bart Platak    schedule 16.12.2016
comment
Итак, предел (sn) где n -> ∞? sn, ты уже реализовал, это (iterate f s)!!n   -  person freestyle    schedule 16.12.2016


Ответы (4)


Я сам играл с этим, решая проблему AoC. Я нашел замечательное решение, которое не требует реверса, поэтому оно более удобно для памяти и быстрее, чем другие решения, перечисленные здесь. Это также красиво! Сама кривая дракона представляет собой хороший короткий двухстрочный код:

merge (x:xs) ys = x:merge ys xs
dragon = merge (cycle [False, True]) dragon

Его можно расширить, чтобы использовать «начальное число», как того требует проблема AoC, просто чередуя начальное число и биты истинной кривой дракона:

infinite bs = go bs (map not (reverse bs)) dragon where
    go bs bs' (d:ds) = bs ++ [d] ++ go bs' bs ds

(Это вызывает reverse один раз, но, в отличие от других решений, он вызывается только один раз для фрагмента данных о размере входных данных, а не повторно для фрагментов данных размером с часть потребляемого вами списка.) Некоторые тайминги, чтобы оправдать мои претензии; все версии, используемые для создания 2 ^ 25 элементов с пустым начальным числом, скомпилированы с ghc -O2 и синхронизированы с /usr/bin/time.

решение от freestyle занимает 11,64 с, макс. ~1,8 Гб резидентного режима
Решение Дэвида Флетчера занимает 10,71 с, макс. ~2 Гб резидентного режима
решение luqui занимает 9,93 с, макс. житель

Полная программа испытаний была

main = mapM_ print . take (2^25) . dragon $ []

с заменой dragon каждой реализацией по очереди. Тщательно продуманный потребитель может еще больше снизить использование памяти: мое лучшее решение проблемы второй звезды на данный момент работает в реальном местоположении 5 МБ (т. ), 60 КБ, о котором сообщает GHC (т. е. только пространство, используемое объектами, еще не подвергнутыми сборке мусора, независимо от того, сколько места GHC выделил из ОС).

Однако по чистой скорости вы не можете превзойти неупакованный изменяемый вектор Bool: коллега сообщает, что его программа, использующая такой, работала за 0,2 с, используя около 35 МБ памяти для хранения полного расширенного (но не бесконечного!) вектора.

person Daniel Wagner    schedule 16.12.2016
comment
Вау, это прекрасно и удивительно - person luqui; 17.12.2016

Вот один из способов. Мы составляем список не из s0, s1 и т. д., а только из новой части на каждом шаге, тогда мы можем просто объединить их вместе.

dragonCurve :: [Bool]
dragonCurve = concatMap f [0..]
  where
    f n = False : (map not . reverse) (take (2^n-1) dragonCurve)

(Это предполагает, что s0 = []. Если это может быть что-то другое, вам придется изменить вычисление длины.)

Я не могу придумать хороший способ быть самореферентным и не иметь дело с длиной префикса. Вот решение без ссылки на себя, все еще использующее идею создания списка непересекающихся частей.

dragonCurve' :: [Bool]
dragonCurve' = concat (unfoldr f [])
  where
    f soFar = Just (part, soFar ++ part)
      where
        part = False : (map not . reverse) soFar
person David Fletcher    schedule 16.12.2016
comment
Спасибо, это именно то, что я искал. Удивительно, но оба этих решения требуют больше памяти, чем моя первоначальная итерация. - person Bart Platak; 16.12.2016
comment
@BartPlatak, одна из причин, по которой может потребоваться больше памяти, заключается в том, что dragonCurve теперь является CAF, поэтому вы навсегда запоминаете список dragonCurve, а не выполняете потоковую передачу и убираете за собой. Хотя асимптотически это должно быть то же самое, потому что reverse уже нужна линейная память, и вы переворачиваете как минимум половину списка. - person luqui; 16.12.2016
comment
Имеет смысл @luqui. Принимая во внимание, что я работал с 35 миллионами значений, это все еще было на удивление быстро. Я также использовал chunksOf позже, что тоже не очень эффективно... - person Bart Platak; 16.12.2016
comment
@BartPlatak, в chunksOf нет ничего страшного. Это не так интересно, как версия, которую я поместил в Data.Sequence, но она работает настолько быстро, насколько это возможно. - person dfeuer; 16.12.2016
comment
@dfeuer: Остальная часть проблемы связана с обработкой непересекающихся пар, сгенерированных из кривой дракона. Я обнаружил, что время вычислений сократилось с 35 с до 9 с, когда я заменил chunksOf на совпадение с образцом - не совсем понимаю, почему. Если вам интересно, разницу можно найти здесь. - person Bart Platak; 16.12.2016
comment
О, для таких крошечных кусков, я думаю, вы, вероятно, много сэкономите, не выделяя куски вообще. chunksOf не имеет смысла в этом контексте. - person dfeuer; 16.12.2016

Ты полный ботаник, подстреливший меня этим. Это не самоссылающийся список, но мне удалось найти «безотходное» решение — такое, при котором мы не отбрасываем и не забываем ничего, что вычислили.

dragon :: [Bool] -> [Bool]
dragon = \s -> s ++ gen [] s
    where
    inv = map not . reverse
    gen a b =
        let b' = inv b
            r = b' ++ a
        in False:r ++ gen r (False:r)

gen a b принимает в качестве входных данных все данные текущей последовательности, так что текущая последовательность равна inv a ++ b. Затем мы генерируем остаток в r, выводим его и рекурсивно продолжаем генерировать остаток. Я принимаю инвертированное a, потому что тогда все, что мне нужно сделать, это добавить b' на каждом этапе (что даже не проверяет a), и нам не нужно инвертировать больше, чем нужно.

Будучи ботаником, я исследовал довольно много других структур данных, полагая, что связанный список, вероятно, не лучший вариант для этой проблемы, включая DList, Data.Sequence (деревья пальцев), свободный моноид (который должен быть хорошо обращен) и пользовательское дерево, которое отменяет реверсы. К моему удивлению, список по-прежнему показал лучшие результаты, и я до сих пор сбит с толку этим. Если вам интересно, вот мой код.

person luqui    schedule 16.12.2016
comment
Мне любопытно, почему списки работают лучше всего. Вы скомпилировали с оптимизацией (т.е. может ли это быть связано со слиянием?). - person Alec; 16.12.2016
comment
Если вам нужна производительность, я рекомендую вручную объединить map not с reverse. revComp = go [] where go acc [] = acc; go acc (x : xs) = let !x' = not x in go (x' : acc) xs. Вероятно, стоит попробовать более существенное преобразование: представить 64 элемента на одно неупакованное слово в data WordList = Cons !Word WordList. Затем вы можете потратить кучу времени на разработку самых быстрых бит-хаков. - person dfeuer; 16.12.2016
comment
Спасибо @luqui, это очень аккуратное (и быстрое!) Решение. Завтра мне придется просмотреть ваш код, чтобы лучше понять различные структуры данных, и вернуться к вам. - person Bart Platak; 16.12.2016
comment
@dfeuer, меня не столько интересовала исходная производительность, сколько то, какая абстракция структуры данных / последовательности будет работать хорошо. Я скомпилировал с оптимизацией, не осознавая, что это может быть связано со слиянием. - person luqui; 16.12.2016

Например:

dragon s0 = s0 ++ concatMap ((False :) . inv) seq
  where
    inv = map not . reverse
    seq = iterate (\s -> s ++ False : inv s) s0 
person freestyle    schedule 16.12.2016
comment
Это неправильное решение: оно создает дополнительную копию первого компонента на каждом рекурсивном уровне. - person luqui; 16.12.2016
comment
@luqui Эта реализация создает последовательность: s0 ++ [False] ++ inv s0 ++ [False] ++ inv s1 ++ [False] ++ inv s2 ++.... Это то, что мы хотим. Так что же не так? - person freestyle; 16.12.2016