Любые указатели на то, как эффективно решить следующую функцию в Haskell, для больших чисел (n > 108)
f(n) = max(n, f(n/2) + f(n/3) + f(n/4))
Я видел примеры запоминания в Хаскеле для решения чисел Фибоначчи, которые включали (лениво) вычисление всех чисел Фибоначчи до требуемого n. Но в этом случае для данного n нам нужно только вычислить очень мало промежуточных результатов.
Спасибо
haskell
memoization
Анхель де Висенте
источник
источник
Ответы:
Мы можем сделать это очень эффективно, создав структуру, которую мы можем индексировать за сублинейное время.
Но сначала,
Давайте определимся
f
, но сделаем так, чтобы он использовал «открытую рекурсию», а не вызывал сам себя.Вы можете получить незапятнанный
f
с помощьюfix f
Это позволит вам протестировать то
f
, что вы имеете в виду, для небольших значенийf
, вызвав, например:fix f 123 = 144
Мы могли бы запомнить это, определив:
Это работает сносно хорошо, и заменяет то, что собиралось занять O (n ^ 3) время чем-то, что запоминает промежуточные результаты.
Но для индексации запомненного ответа все равно требуется линейное время
mf
. Это означает, что результаты как:терпимы, но результат не намного лучше, чем это. Мы можем сделать лучше!
Сначала давайте определим бесконечное дерево:
И тогда мы определим способ индексации в нем, чтобы вместо этого мы могли найти узел с индексом
n
за O (log n) :... и мы можем найти удобное дерево, полное натуральных чисел, поэтому нам не нужно возиться с этими индексами:
Поскольку мы можем индексировать, вы можете просто преобразовать дерево в список:
Вы можете проверить работу до сих пор, убедившись, что
toList nats
дает вам[0..]
Сейчас,
работает так же, как и в приведенном выше списке, но вместо того, чтобы тратить время на линейное нахождение каждого узла, можно найти его в логарифмическом времени.
Результат значительно быстрее:
На самом деле это намного быстрее, что вы можете перейти и заменить
Int
наInteger
выше и получить смехотворно большие ответы почти мгновенноисточник
f_tree
следует определять вwhere
предложении, чтобы избежать сохранения ненужных путей в дереве между вызовами?Ответ Эдварда настолько удивителен, что я продублировал его и предоставил реализации
memoList
иmemoTree
комбинаторы, которые запоминают функцию в открытой рекурсивной форме.источник
Не самый эффективный способ, но запоминает
при запросе
f !! 144
проверяется, чтоf !! 143
существует, но его точное значение не рассчитывается. Это все еще установлено как некоторый неизвестный результат вычисления. Единственные точные рассчитанные значения являются необходимыми.Итак, изначально, насколько было рассчитано, программа ничего не знает.
Когда мы делаем запрос
f !! 12
, он начинает выполнять сопоставление с шаблоном:Теперь начинается расчет
Это рекурсивно выдвигает другое требование на f, поэтому мы вычислим
Теперь мы можем сделать немного
Что означает, что программа теперь знает:
Продолжая сочиться:
Что означает, что программа теперь знает:
Теперь мы продолжим наш расчет
f!!6
:Что означает, что программа теперь знает:
Теперь мы продолжим наш расчет
f!!12
:Что означает, что программа теперь знает:
Так что расчет сделан довольно лениво. Программа знает, что какое-то значение для
f !! 8
существует, что оно равноg 8
, но она не знает, что этоg 8
такое.источник
g n m = (something with) f!!a!!b
Это дополнение к прекрасному ответу Эдварда Кметта.
Когда я попробовал его код, определения
nats
иindex
казались довольно загадочными, поэтому я написал альтернативную версию, которая мне показалась более легкой для понимания.Я определяю
index
иnats
с точки зренияindex'
иnats'
.index' t n
определяется в диапазоне[1..]
. (Вспомните, чтоindex t
определено в диапазоне[0..]
.) Он работает, ищет дерево, обрабатывая егоn
как цепочку битов и считывая биты в обратном порядке. Если бит равен1
, он принимает правую ветвь. Если бит равен0
, он принимает левую ветвь. Он останавливается, когда достигает последнего бита (который должен быть1
).Так же, как
nats
определено дляindex
так, чтоindex nats n == n
всегда верно,nats'
определено дляindex'
.Теперь,
nats
иindex
простоnats'
и ,index'
но со значениями сдвинуты на 1:источник
Как указано в ответе Эдварда Кметта, чтобы ускорить процесс, вам необходимо кэшировать дорогостоящие вычисления и иметь быстрый доступ к ним.
Чтобы сохранить функцию не монадической, решение этой задачи заключается в создании бесконечного ленивого дерева с соответствующим способом его индексации (как показано в предыдущих статьях). Если вы отказываетесь от немонадной природы функции, вы можете использовать стандартные ассоциативные контейнеры, доступные в Haskell, в сочетании с «подобными состоянию» монадами (такими как State или ST).
Хотя основным недостатком является то, что вы получаете немонадную функцию, вам больше не нужно индексировать структуру самостоятельно, и вы можете просто использовать стандартные реализации ассоциативных контейнеров.
Для этого сначала нужно переписать свою функцию, чтобы она принимала любую монаду:
Для ваших тестов вы все равно можете определить функцию, которая не запоминает, используя Data.Function.fix, хотя она немного более многословна:
Затем вы можете использовать State monad в сочетании с Data.Map, чтобы ускорить процесс:
С небольшими изменениями вы можете вместо этого адаптировать код для работы с Data.HashMap:
Вместо постоянных структур данных вы также можете использовать изменяемые структуры данных (например, Data.HashTable) в сочетании с монадой ST:
По сравнению с реализацией без какого-либо запоминания, любая из этих реализаций позволяет вам при огромных входах получать результаты в микросекундах вместо того, чтобы ждать несколько секунд.
Используя Criterion в качестве эталона, я мог заметить, что реализация с Data.HashMap на самом деле работала немного лучше (около 20%), чем Data.Map и Data.HashTable, для которых сроки были очень похожи.
Я нашел результаты теста немного удивительными. Сначала я чувствовал, что HashTable превзойдет реализацию HashMap, потому что она изменчива. В этой последней реализации может быть скрыт некоторый дефект производительности.
источник
Пару лет спустя я посмотрел на это и понял, что есть простой способ запомнить это в линейном времени, используя
zipWith
вспомогательную функцию:dilate
имеет удобное свойство, чтоdilate n xs !! i == xs !! div i n
.Итак, предположим, что нам дано f (0), это упрощает вычисление до
Очень похоже на наше оригинальное описание проблемы и дает линейное решение (
sum $ take n fs
потребуется O (n)).источник
Еще одно дополнение к ответу Эдварда Кметта: отдельный пример:
Используйте его следующим образом, чтобы запомнить функцию с одним целочисленным аргументом (например, fibonacci):
Будут кэшироваться только значения для неотрицательных аргументов.
Чтобы также кэшировать значения для отрицательных аргументов, используйте
memoInt
следующее:Для кэширования значений для функций с двумя целочисленными аргументами используйте
memoIntInt
следующее:источник
Решение без индексации и не основано на Эдварде КМЕТТ.
Я выделяю общие поддеревья для общего родителя (
f(n/4)
разделяется междуf(n/2)
иf(n/4)
, иf(n/6)
разделяется междуf(2)
иf(3)
). Сохраняя их как одну переменную в родительском объекте, вычисление поддерева выполняется один раз.Код не может легко распространяться на общую функцию запоминания (по крайней мере, я бы не знал, как это сделать), и вам действительно нужно подумать, как перекрываются подзадачи, но стратегия должна работать для общих нескольких нецелых параметров. , (Я придумал это для двух строковых параметров.)
Записка сбрасывается после каждого расчета. (Опять же, я думал о двух строковых параметрах.)
Я не знаю, является ли это более эффективным, чем другие ответы. Технически каждый поиск - это всего лишь один или два шага («Посмотрите на вашего ребенка или ребенка вашего ребенка»), но может потребоваться много дополнительной памяти.
Изменить: Это решение еще не является правильным. Разделение неполное.Редактировать: Теперь он должен делиться своими детьми должным образом, но я понял, что эта проблема имеет много нетривиального обмена:
n/2/2/2
иn/3/3
может быть такой же. Проблема не подходит для моей стратегии.источник