Запоминаемая, рекурсивная факториальная функция?

Я знаю, как легко делать мемоизации в Python, но мне нужен более быстрый способ их вычисления, поэтому я использую C++. Однако я понятия не имею, как запоминать. Я понимаю, что речь идет о сохранении значений в массив или вектор, а затем о сканировании его значения при извлечении, но было бы очень полезно посмотреть, как это делается, чтобы я мог попробовать его скорость.


person John Smith    schedule 15.03.2012    source источник
comment
Я не минусовал. Но я уверен, что нет. В отличие от рекурсивного алгоритма Фибоначчи, для факторного алгоритма нет никакой выгоды от запоминания.   -  person Mysticial    schedule 16.03.2012
comment
@Mystical: позволю себе не согласиться. Последовательность Фибоначчи может быть записана в алгоритме O(n) точно так же, как вычисление факториала. Компромисс с мемоизацией занимает O(n) памяти для O(1) операций поиска. Быстро сделать n умножения или сложения (где n относительно мало). Но если вы постоянно вызываете его, вам может помочь запоминание.   -  person Mike Bailey    schedule 16.03.2012
comment
@MikeBantegui Фибоначчи можно рассчитать в O(pow), где pow — это сложность вашей power() функции. Для этого есть закрытая формула.   -  person amit    schedule 16.03.2012
comment
@MikeBantegui Я говорю о рекурсивном алгоритме O(2^n) для Фибоначчи. Реализация, которую вы делаете для новичков в C++ с целью изучения рекурсии. Это не должно быть эффективным.   -  person Mysticial    schedule 16.03.2012
comment
@amit: Я хочу сказать, что рассуждение о том, что Фибоначчи может извлечь из этого пользу, но не факториал, упускает из виду тот факт, что оба решения имеют экспоненциальную и линейную временную сложность в зависимости от того, какую версию вы используете. Факториал может быть выполнен в O(pow) и для больших значений n благодаря аппроксимации Стирлинга. Я не обсуждал временные сложности, а просто то, что запоминание может принести пользу факториалу и числам Фибоначчи   -  person Mike Bailey    schedule 16.03.2012
comment
Как бы вы сделали это в Python? C++ более или менее идентичен, за исключением того, что у вас нет красивого синтаксиса декоратора.   -  person Konrad Rudolph    schedule 16.03.2012
comment
Я делаю это с синтаксисом декоратора, поэтому я не был уверен, как это перевести.   -  person John Smith    schedule 16.03.2012
comment
Здесь в плохо написанном скрывается достойный вопрос. Мемоизация — это метод, который легко реализовать почти незаметно в одних языках и требует дополнительной работы в других. Я не вижу ничего плохого в том, чтобы задаться вопросом, как сделать это наиболее удобным и универсальным способом с помощью C++.   -  person DavidO    schedule 16.03.2012
comment
Вот рабочая версия: ideone.com/up6Vy и несколько других здесь: cpptruths.blogspot.de/2012/01/ хотя и с Фибоначчи   -  person Dmitry Ledentsov    schedule 23.01.2013
comment
Это похоже на трудную проблему. Ответы @KerrekSB и @CharlesKeepax ниже не работают для рекурсивных функций: попробуйте запомнить рекурсивную функцию factorial и обратите внимание на ноль попаданий в кеш для одного вызова, например. mem<int, factorial>(5);   -  person Senti Bachcha    schedule 22.04.2014


Ответы (4)


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

template <typename T, T (*calc)(T)>
class mem {
  std::map<T,T> mem_map;

public:
  T operator()(T input) {
    typename std::map<T,T>::iterator it;

    it = mem_map.find(input);
    if (it != mem_map.end()) {
      return it->second;
    } else {
      T output = calc(input);
      mem_map[input] = output;
      return output;
    }
  }
};

Код определяет класс-шаблон, который принимает имя типа и указатель на функцию, затем в классе определяется оператор функции, позволяющий его вызывать. Оператор функции принимает входное значение, проверяет, находится ли указанное значение на карте, затем либо просто возвращает его из карты, либо вычисляет его (используя указатель функции), добавляет его на карту и затем возвращает.

Итак, если вы определяете некоторую функцию обработки, например:

int unity(int in) { return in; }

Вы бы использовали такой код:

mem<int, unity> mem_unity;
int y;
y = mem_unity(10);

Итак, мы определяем экземпляр класса mem, который принимает наш тип значения и функцию обработки в качестве параметров шаблона, а затем просто вызывает этот класс как функцию.

person Charles Keepax    schedule 15.03.2012
comment
Это не работает. Первый вызов calc() вызывает необработанный, не запомненный calc, и если он рекурсивный, кеш больше никогда не просматривается. - person Senti Bachcha; 22.04.2014
comment
Честно говоря, для повторяющихся поисков это действительно работает; но OP хотел, чтобы мемоизация ускорила рекурсивную функцию. Это крайне необходимо для рекурсивных функций, например. factorial(n) для больших n или в решениях динамического программирования. - person Senti Bachcha; 22.04.2014

Просто для удовольствия, вот небольшой общий мемоайзер, который я написал некоторое время назад. Естественно, для этого требуются вариативные шаблоны:

template <template <typename...> class Container, typename...> struct Memo;

template <typename R, typename... Args, template <typename...> class Container>
struct Memo<Container, R, std::tuple<Args...>>
{
  Memo(std::function<R(Args...)> f) : func(f) { }

  R operator()(Args && ...args)
  {
    const auto arg = std::make_tuple(args...);
    typename CacheContainer::const_iterator it = cache.find(arg);

    if (it == cache.cend())
    {
      it = cache.insert(typename CacheContainer::value_type(arg, func(std::forward<Args>(args)...))).first;
      std::cout << "New call, memoizing..." << std::endl;
    }
    else
    {
      std::cout << "Found it in the cache!" << std::endl;
    }

    return it->second;
  }

private:

  typedef Container<typename std::tuple<Args...>, R> CacheContainer;

  std::function<R(Args...)> func;
  CacheContainer cache;
};


template <typename R, typename... Args>
Memo<std::map, R, std::tuple<Args...>> OMapMemoize(R(&f)(Args...))
{
  return Memo<std::map, R, std::tuple<Args...>>(f);
}
template <typename R, typename... Args>
Memo<std::unordered_map, R, std::tuple<Args...>> UMapMemoize(R(&f)(Args...))
{
  return Memo<std::unordered_map, R, std::tuple<Args...>>(f);
}

Я не совсем уверен, правильно ли я понял rvalue-forwardiness, так как написал это давным-давно. В любом случае, вот тестовый пример:

int foo(double, char) { return 5; }

int main()
{
  auto f = OMapMemoize(foo);
  auto g = UMapMemoize(foo);

  int a, b;

  a = f(1.0, 'a');
  a = f(1.0, 'a');
  a = f(1.0, 'a');
  a = f(1.0, 'a');

  b = g(1.0, 'a');
  b = g(1.0, 'a');
  b = g(1.0, 'a');
  b = g(1.0, 'a');

  return a;
}
person Kerrek SB    schedule 15.03.2012
comment
Значение r в порядке, по крайней мере, для вещей, на которых я пробовал ваш мемоайзер. ;). - person CoffeDeveloper; 21.10.2014
comment
@DarioOO: Спасибо за проверку! - person Kerrek SB; 21.10.2014

Никто, кроме студента, изучающего рекурсию, не будет вычислять факториалы таким образом.

Мемоизация — очень хорошая идея, особенно если вы собираетесь многократно вызывать метод. Зачем выбрасывать хорошую работу?

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

Но, во что бы то ни стало, запомните любую реализацию, которую вы используете. Если вы пишете его на C++, я бы рекомендовал использовать std:map с аргументом x в качестве ключа и ln(gamma(x)) в качестве значения.

Извините, я слишком давно не писал C++ и STL. Я бы предпочел использовать хэш-карту с временем доступа для чтения O(1), чем перебирать ключи в O(n).

person duffymo    schedule 15.03.2012

Мне нравится полагаться на захват лямбда, как в (использует std=c++14)

template<typename R, typename... Args>
auto memoize(std::function<R(Args...)>&& f)
{
  using F = std::function<R(Args...)>;
  std::map<std::tuple<Args...>,R> cache;
  return ([cache = std::map<std::tuple<Args...>,R>{}, 
           f = std::forward<F>(f)](Args&&... args) mutable
    {
      std::tuple<Args...> t(args...);
      if (cache.find(t) == cache.end())
        {
          R r = f(std::forward<Args...>(args)...);
          cache[t] = r;
        }
      return cache[t];
    });
}
person Charles Pehlivanian    schedule 28.03.2016
comment
проблема в том, что внутренние вызовы не проходят через мемуайзер. - person Johan Lundberg; 09.07.2017