11

我正在编写一个函数来查找三角形数,而编写它的自然方法是递归:

function triangle (x)
   if x == 0 then return 0 end
   return x+triangle(x-1)
end

但是尝试计算前 100,000 个三角形数会在一段时间后因堆栈溢出而失败。这是memoize的理想函数,但我想要一个能够记住我传递给它的任何函数的解决方案。

4

15 回答 15

9

Mathematica 有一种特别巧妙的方式来做记忆,依赖于哈希和函数调用使用相同的语法这一事实:

triangle[0] = 0;
triangle[x_] := triangle[x] = x + triangle[x-1]

就是这样。它之所以有效,是因为模式匹配函数调用的规则是,它总是在更一般的定义之前使用更具体的定义。

当然,正如已经指出的那样,这个例子有一个封闭形式的解决方案:triangle[x_] := x*(x+1)/2. 斐波那契数是添加记忆如何显着加速的经典示例:

fib[0] = 1;
fib[1] = 1;
fib[n_] := fib[n] = fib[n-1] + fib[n-2]

虽然这也有一个封闭形式的等价物,尽管更混乱: http: //mathworld.wolfram.com/FibonacciNumber.html

我不同意那个建议这不适合记忆的人,因为你可以“只使用一个循环”。记忆的要点是任何重复的函数调用都是 O(1) 时间。这比 O(n) 好很多。事实上,您甚至可以编造一个场景,其中 memoized 的实现比封闭形式的实现具有更好的性能!

于 2008-10-06T02:17:23.290 回答
6

您还为原始问题提出了错误的问题;)

对于这种情况,这是一种更好的方法:

三角形(n) = n * (n - 1) / 2

此外,假设公式没有这样一个简洁的解决方案,记忆仍然是一个糟糕的方法。在这种情况下,您最好只编写一个简单的循环。请参阅此答案以进行更全面的讨论。

于 2008-09-24T20:56:50.580 回答
5

我敢打赌,这样的事情应该适用于 Lua 中的变量参数列表:

local function varg_tostring(...)
    local s = select(1, ...)
    for n = 2, select('#', ...) do
        s = s..","..select(n,...)
    end
    return s
end

local function memoize(f)
    local cache = {}
    return function (...)
        local al = varg_tostring(...)
        if cache[al] then
            return cache[al]
        else
            local y = f(...)
            cache[al] = y
            return y
        end
    end
end

您可能还可以使用带有 __tostring 的元表做一些聪明的事情,以便可以使用 tostring() 转换参数列表。哦,可能性。

于 2008-09-26T20:15:21.837 回答
5

在 C# 3.0 - 对于递归函数,您可以执行以下操作:

public static class Helpers
{
    public static Func<A, R> Memoize<A, R>(this Func<A, Func<A,R>,  R> f)
    {
        var map = new Dictionary<A, R>();
        Func<A, R> self = null;
        self = (a) =>
        {
            R value;
            if (map.TryGetValue(a, out value))
                return value;
            value = f(a, self);
            map.Add(a, value);
            return value;
        };
        return self;
    }        
}

然后你可以创建一个这样的记忆斐波那契函数:

var memoized_fib = Helpers.Memoize<int, int>((n,fib) => n > 1 ? fib(n - 1) + fib(n - 2) : n);
Console.WriteLine(memoized_fib(40));
于 2011-11-20T20:13:31.507 回答
4

在 Scala 中(未经测试):

def memoize[A, B](f: (A)=>B) = {
  var cache = Map[A, B]()

  { x: A =>
    if (cache contains x) cache(x) else {
      val back = f(x)
      cache += (x -> back)

      back
    }
  }
}

请注意,这仅适用于 arity 1 的函数,但通过 currying 您可以使其工作。更微妙的问题是memoize(f) != memoize(f)对于任何函数f。解决此问题的一种非常偷偷摸摸的方法如下:

val correctMem = memoize(memoize _)

我认为这不会编译,但它确实说明了这个想法。

于 2008-09-24T20:54:17.637 回答
4

更新:评论者指出,记忆是优化递归的好方法。诚然,我以前没有考虑过这一点,因为我通常使用一种语言 (C#),在这种语言中,通用的记忆化并不是那么容易构建的。牢记那粒盐,拿下面的帖子。

我认为Luke 可能对这个问题有最合适的解决方案,但记忆化通常不是任何堆栈溢出问题的解决方案。

堆栈溢出通常是由于递归超出平台可以处理的深度引起的。语言有时支持“尾递归”,它重用当前调用的上下文,而不是为递归调用创建新的上下文。但是很多主流语言/平台不支持这一点。例如,C# 没有对尾递归的固有支持。.NET JITter 的 64 位版本可以将其用作 IL 级别的优化,如果您需要支持 32 位平台,这几乎是无用的。

如果您的语言不支持尾递归,那么避免堆栈溢出的最佳选择是转换为显式循环(不太优雅,但有时​​是必要的),或者找到一个非迭代算法,例如为这个问题提供的 Luke。

于 2008-09-24T21:10:04.917 回答
3
function memoize (f)
   local cache = {}
   return function (x)
             if cache[x] then
                return cache[x]
             else
                local y = f(x)
                cache[x] = y
                return y
             end
          end
end

triangle = memoize(triangle);

请注意,为避免堆栈溢出,三角形仍需要播种。

于 2008-09-24T20:49:08.303 回答
2

这是无需将参数转换为字符串即可工作的东西。唯一需要注意的是它无法处理 nil 参数。但是公认的解决方案无法区分 valuenil和 string "nil",所以这可能没问题。

local function m(f)
  local t = { }
  local function mf(x, ...) -- memoized f
    assert(x ~= nil, 'nil passed to memoized function')
    if select('#', ...) > 0 then
      t[x] = t[x] or m(function(...) return f(x, ...) end)
      return t[x](...)
    else
      t[x] = t[x] or f(x)
      assert(t[x] ~= nil, 'memoized function returns nil')
      return t[x]
    end
  end
  return mf
end
于 2010-09-06T19:36:36.857 回答
2

我受到这个问题的启发,在 Lua 中实现(又一个)灵活的 memoize 功能。

https://github.com/kikito/memoize.lua

主要优点:

  • 接受可变数量的参数
  • 不使用 tostring;相反,它以树结构组织缓存,使用参数对其进行遍历。
  • 适用于返回多个值的函数。

在此处粘贴代码作为参考:

local globalCache = {}

local function getFromCache(cache, args)
  local node = cache
  for i=1, #args do
    if not node.children then return {} end
    node = node.children[args[i]]
    if not node then return {} end
  end
  return node.results
end

local function insertInCache(cache, args, results)
  local arg
  local node = cache
  for i=1, #args do
    arg = args[i]
    node.children = node.children or {}
    node.children[arg] = node.children[arg] or {}
    node = node.children[arg]
  end
  node.results = results
end


-- public function

local function memoize(f)
  globalCache[f] = { results = {} }
  return function (...)
    local results = getFromCache( globalCache[f], {...} )

    if #results == 0 then
      results = { f(...) }
      insertInCache(globalCache[f], {...}, results)
    end

    return unpack(results)
  end
end

return memoize
于 2011-04-20T22:07:37.087 回答
1

这是一个通用的 C# 3.0 实现,如果有帮助的话:

public static class Memoization
{
    public static Func<T, TResult> Memoize<T, TResult>(this Func<T, TResult> function)
    {
        var cache = new Dictionary<T, TResult>();
        var nullCache = default(TResult);
        var isNullCacheSet = false;
        return  parameter =>
                {
                    TResult value;

                    if (parameter == null && isNullCacheSet)
                    {
                        return nullCache;
                    }

                    if (parameter == null)
                    {
                        nullCache = function(parameter);
                        isNullCacheSet = true;
                        return nullCache;
                    }

                    if (cache.TryGetValue(parameter, out value))
                    {
                        return value;
                    }

                    value = function(parameter);
                    cache.Add(parameter, value);
                    return value;
                };
    }
}

(引自法国博客文章

于 2008-09-24T20:53:01.493 回答
1

在以不同语言发布备忘录的情况下,我想用一个不改变语言的 C++ 示例来回复@onebyone.livejournal.com。

首先,单个 arg 函数的记忆器:

template <class Result, class Arg, class ResultStore = std::map<Arg, Result> >
class memoizer1{
public:
    template <class F>
    const Result& operator()(F f, const Arg& a){
        typename ResultStore::const_iterator it = memo_.find(a);
        if(it == memo_.end()) {
            it = memo_.insert(make_pair(a, f(a))).first;
        }
        return it->second;
    }
private:
    ResultStore memo_;
};

只需创建一个 memoizer 实例,将您的函数和参数提供给它。只要确保不要在两个不同的函数之间共享相同的备忘录(但您可以在同一函数的不同实现之间共享它)。

接下来,一个驱动函数和一个实现。只有驱动函数需要 public int fib(int); // 驱动程序 int fib_(int); // 执行

实施的:

int fib_(int n){
    ++total_ops;
    if(n == 0 || n == 1) 
        return 1;
    else
        return fib(n-1) + fib(n-2);
}

和司机,记住

int fib(int n) {
    static memoizer1<int,int> memo;
    return memo(fib_, n);
}

永久链接在 codepad.org 上显示输出。测量调用次数以验证正确性。(在此处插入单元测试...)

这只会记住一个输入函数。概括多个参数或不同的参数作为练习留给读者。

于 2008-09-27T22:31:58.270 回答
1

在 Perl 中,通用记忆很容易获得。Memoize 模块是 perl 核心的一部分,非常可靠、灵活且易于使用。

它的手册页中的示例:

# This is the documentation for Memoize 1.01
use Memoize;
memoize('slow_function');
slow_function(arguments);    # Is faster than it was before

您可以在运行时添加、删除和自定义函数的记忆您可以为自定义纪念品计算提供回调。

Memoize.pm 甚至具有使 memento 缓存持久化的功能,因此无需在每次调用程序时重新填充!

这是文档: http: //perldoc.perl.org/5.8.8/Memoize.html

于 2008-11-04T20:26:01.700 回答
0

扩展这个想法,还可以使用两个输入参数来记忆函数:

function memoize2 (f)
   local cache = {}
   return function (x, y)
             if cache[x..','..y] then
                return cache[x..','..y]
             else
                local z = f(x,y)
                cache[x..','..y] = z
                return z
             end
          end
end

请注意,参数顺序在缓存算法中很重要,因此如果参数顺序在要记忆的函数中无关紧要,则通过在检查缓存之前对参数进行排序来增加缓存命中的几率。

但重要的是要注意,某些功能无法以有利可图的方式进行记忆。我写了memoize2来看看是否可以加快寻找最大公约数的递归欧几里得算法。

function gcd (a, b) 
   if b == 0 then return a end
   return gcd(b, a%b)
end

事实证明,gcd对记忆的反应并不好。它所做的计算比缓存算法便宜得多。对于大量数据,它会很快终止。一段时间后,缓存变得非常大。该算法可能尽可能快。

于 2008-09-26T18:50:05.457 回答
0

递归不是必需的。第 n 个三角形数是 n(n-1)/2,所以...

public int triangle(final int n){
   return n * (n - 1) / 2;
}
于 2011-12-06T04:42:46.383 回答
0

请不要递归这个。要么使用 x*(x+1)/2 公式,要么简单地迭代值并随时记忆。

int[] memo = new int[n+1];
int sum = 0;
for(int i = 0; i <= n; ++i)
{
  sum+=i;
  memo[i] = sum;
}
return memo[n];
于 2013-04-06T01:45:43.673 回答