Это вопрос, который я прочитал на некоторых лекциях о динамическом программировании, которые случайно нашел в Интернете. (Я закончил и уже знаю основы динамического программирования)

В разделе объяснения, зачем нужна мемоизация, т.е.

// psuedo code 
int F[100000] = {0};
int fibonacci(int x){
    if(x <= 1) return x;
    if(F[x]>0) return F[x];
    return F[x] = fibonacci(x-1) + fibonacci(x-2);
}

Если мемоизация не используется, то многие подзадачи будут многократно пересчитываться, что делает сложность очень высокой.


Затем на одной странице в заметках есть вопрос без ответа, и это именно то, что я хочу задать. Здесь я использую точные формулировки и примеры, которые они показывают:

Автоматическая мемоизация: многие языки функционального программирования (например, Lisp) имеют встроенную поддержку мемоизации.

Почему не в императивных языках (например, Java)?

Пример LISP, приведенный в примечании (который, как утверждается, является эффективным):

(defun F (n)
    (if
        (<= n 1)
        n
        (+ (F (- n 1)) (F (- n 2)))))

Он предоставляет пример Java (который, по его утверждению, является экспоненциальным)

static int F(int n) {
  if (n <= 1) return n;
  else return F(n-1) + F(n-2);
}

Прежде чем это прочитать, я даже не знал, что в некоторых языках программирования есть встроенная поддержка мемоизации.

Верно ли утверждение в примечаниях? Если да, то почему императивные языки не поддерживают его?

7
shole 7 Сен 2016 в 10:39

3 ответа

Лучший ответ

Утверждения о "LISP" очень расплывчаты, они даже не упоминают, какой диалект LISP или его реализацию имеют в виду. Ни один из знакомых мне диалектов LISP не поддерживает автоматическую мемоизацию, но LISP позволяет легко написать функцию-оболочку, которая преобразует любую существующую функцию в мемоизированную.

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

5
Marko Topolnik 7 Сен 2016 в 08:37

Поддержка мемоизации - это не что иное, как наличие первоклассных функций.

Если вы хотите запоминать версию Java для одного конкретного случая, вы можете написать ее явно: создать хеш-таблицу, проверить существующие значения и т.д. К сожалению, вы не можете легко обобщить это, чтобы запоминать любую функцию. Языки с первоклассными функциями делают написание функций и их запоминание почти ортогональными проблемами.

Базовый случай прост, но вы должны учитывать рекурсивные вызовы. В статически типизированных функциональных языках, таких как OCaml, мемоизированная функция не может просто вызывать себя рекурсивно, потому что она вызовет немомоизованную версию. Однако единственное изменение в вашей существующей функции - это принять функцию в качестве аргумента, например, с именем self, которую следует вызывать всякий раз, когда вы хотите выполнить рекурсию функции. Затем универсальное средство мемоизации предоставляет соответствующую функцию. Полный пример этого доступен в этом ответе.

Версия Lisp имеет две особенности, которые делают запоминание существующей функции еще более простым.

  1. Вы можете управлять функциями как любым другим значением
  2. Вы можете переопределить функции во время выполнения

Так, например, в Common Lisp вы определяете F:

(defun F (n)
  (if (<= n 1)
      n
      (+ (F (- n 1))
         (F (- n 2)))))

Затем вы видите, что вам нужно запомнить функцию, поэтому вы загружаете библиотеку:

(ql:quickload :memoize) 

... и запомни F:

(org.tfeb.hax.memoize:memoize-function 'F)

Средство принимает аргументы, чтобы указать, какие входные данные следует кэшировать и какую тестовую функцию использовать. Затем функция F заменяется новой, которая вводит необходимый код для использования внутренней хеш-таблицы. Рекурсивные вызовы F внутри F теперь вызывают функцию упаковки, а не исходную (вы даже не перекомпилируете F). Единственная потенциальная проблема заключается в том, что исходный F подвергался оптимизации хвостового вызова. Вам, вероятно, следует объявить его notinline или использовать DEF-MEMOIZED-FUNCTION.

3
Community 23 Май 2017 в 10:32

Хотя я не уверен, что какой-либо широко используемый Lisp поддерживает автоматическую мемоизацию, я думаю, что есть две причины, по которым мемоизация более распространена в функциональных языках, и еще одна причина для языков семейства Lisp.

Прежде всего, люди пишут функции на функциональных языках: вычисления, результат которых зависит только от их аргументов и которые не влияют на среду. Все, что не соответствует этому требованию, вообще не поддается мемоизации. И, ну, императивные языки - это как раз те языки, в которых эти требования не выполняются или могут не выполняться, потому что в противном случае они не были бы императивными!

Конечно, даже с простыми функционально-дружественными языками, такими как (большинство) Lisps, вы должны быть осторожны: вам, вероятно, не следует запоминать следующее, например:

(defvar *p* 1)

(defun foo (n)
  (if (<= n 0)
      *p*
    (+ (foo (1- n)) (foo (- n *p*)))))

Во-вторых, функциональные языки обычно хотят говорить о неизменяемых структурах данных. Это означает две вещи:

  1. На самом деле безопасно запоминать функцию, которая возвращает большую структуру данных.
  2. Функции, которые создают очень большие структуры данных, часто нуждаются в огромном количестве мусора, потому что они не могут изменять промежуточные структуры.

(2) немного противоречиво: общепринятая мудрость заключается в том, что сборщики мусора теперь настолько хороши, что это не проблема, копирование обходится очень дешево, компиляторы могут творить чудеса и так далее. Что ж, люди, которые написали такие функции, будут знать, что это верно лишь отчасти: сборщики мусора хороши, копирование - это дешево (но поиск больших структур для их копирования часто бывает очень враждебно настроен по отношению к кешам), но на самом деле этого недостаточно (а компиляторы почти никогда не делают то волшебство, на которое они претендуют). Итак, вы либо обманываете, необоснованно прибегая к нефункциональному коду, либо запоминаете. Если вы запомните функцию, тогда вы создадите все промежуточные структуры только один раз, и все станет дешевым (кроме памяти, но подходящая слабость в мемоизации может справиться с этим).

В-третьих: если ваш язык не поддерживает простую металингвистическую абстракцию, реализация мемоизации - серьезная проблема. Или, другими словами: вам нужны макросы в стиле Lisp.

Чтобы запоминать функцию, вам нужно сделать как минимум две вещи:

  1. Вам необходимо контролировать, какие аргументы являются ключами для мемоизации - не все функции имеют только один аргумент, и не все функции с несколькими аргументами должны быть мемоизированы с первым;
  2. Вам нужно вмешаться внутри функции, чтобы отключить любую оптимизацию автоматического вызова, которая полностью разрушит мемоизацию.

Хотя это довольно жестоко, потому что это так просто, я продемонстрирую это, подшучивая над Python.

Вы можете подумать, что декораторы - это то, что вам нужно для запоминания функций в Python. И действительно, вы можете писать инструменты мемоизации, используя декораторы (а я написал их кучу). И это даже вроде работы, хотя в большинстве случаев это случается.

Во-первых, декоратор не может легко узнать что-либо о функции, которую он украшает. Таким образом, вы либо пытаетесь запоминать на основе кортежа всех аргументов функции, либо указываете в декораторе, какие аргументы запоминать, либо что-то столь же уродливое.

Во-вторых, декоратор получает функцию, которую он украшает, в качестве аргумента: он не может ковыряться внутри нее. На самом деле это нормально, потому что Python, как часть своей политики «никаких концепций, изобретенных после 1956 года», конечно, не предполагает, что вызовы f лексически входят в определение f (и без промежуточных привязок ) на самом деле являются телефонными звонками. Но, возможно, однажды это произойдет, и все ваши воспоминания сейчас сломаются.

Итак, вкратце: чтобы надежно запоминать функции, вам нужны макросы в стиле Lisp. Вероятно, единственные императивные языки, в которых они есть, - это Лисп.

1
tfb 7 Сен 2016 в 11:36