如何编写通用的记忆函数?

How do I write a generic memoize function?

提问人:Jon 'links in bio' Ericson 提问时间:9/25/2008 更新时间:4/6/2013 访问量:9336

问:

我正在编写一个函数来查找三角形数字,并且自然的编写方式是递归:

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

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

优化 递归 Lua 闭包记忆

评论

2赞 Fractaly 12/6/2011
为什么每个人都在编写复杂、缓慢的递归函数?它只是 n*(n-1)/2。请参阅下面的回答。它是用 java 而不是 Lua 语言表达的,但仍然传达了这个想法。
1赞 Jon 'links in bio' Ericson 12/6/2011
@Fractaly:非常正确。然而,功能并不是问题的主旨,它实际上是在问一个稍微深奥的概念。不过,这是解决问题的最佳方法。事实上,它太好了,以至于已经有人建议了。triangle
0赞 Fractaly 12/7/2011
感谢您指出这一点。有很多答案,我想我错过了。这似乎是记忆的一个糟糕的例子,因为它实际上并不需要它。
0赞 Jon 'links in bio' Ericson 12/8/2011
@Fractaly:那时我还年轻,又傻......
0赞 thSoft 10/29/2009
请参阅这篇博文,了解通用的 Scala 解决方案,最多 4 个参数。

答:

3赞 Jon 'links in bio' Ericson 9/25/2008 #1
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);

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

评论

0赞 Adam Rosenfield 9/25/2008
一个有趣(但无用)的构造,具有通用的记忆函数:在记忆上调用记忆
0赞 Jon 'links in bio' Ericson 9/25/2008
@亚当·罗森菲尔德:嗯......时髦!
1赞 Romain Verdier 9/25/2008 #2

这是一个通用的 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;
                };
    }
}

(引自一篇法国博客文章)

4赞 Daniel Spiewak 9/25/2008 #3

在 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 的函数,但通过咖喱,您可以使其工作。更微妙的问题是,对于任何函数。解决此问题的一种非常狡猾的方法是如下所示:memoize(f) != memoize(f)f

val correctMem = memoize(memoize _)

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

评论

0赞 Daniel Spiewak 9/25/2008
哈哈 好点子,我的陈述还不够。我会修复它。
0赞 RCIX 9/6/2009
至少在我看来,scala 看起来像是 Python、c# 和 c++ 的怪兽。
6赞 Luke Halliwell 9/25/2008 #4

你也问了错误的问题,因为你原来的问题;)

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

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

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

评论

0赞 Jon 'links in bio' Ericson 9/25/2008
玩弄这个函数,似乎很明显会有一个更简单的算法。谢谢!
0赞 Jon 'links in bio' Ericson 9/25/2008
@onebyone.livejournal.com:我敢肯定,当我解决问题时,笔记会揭示这个数学解决方案。;-)
4赞 Chris Ammerman 9/25/2008 #5

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

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

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

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

评论

0赞 Jon 'links in bio' Ericson 9/25/2008
实际上,即使尾部递归生效,也应该记住这个函数。为了说服自己,想象一下用两个非常大的数字调用它两次。如果缓存了第一次调用的结果,则第二次调用的速度会快得多。
0赞 Alexandre C. 11/14/2010
还有不实现尾调用优化的语言吗?我以为那些Microsoft家伙做了 F#,他们一定在任何地方都实现了它。
0赞 Jon 'links in bio' Ericson 9/27/2008 #6

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

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 对记忆的反应并不好。它所做的计算远比缓存算法便宜。对于大数字,它很快就会终止。一段时间后,缓存变得非常大。这个算法可能尽可能快。

评论

0赞 Lee Baldwin 9/27/2008
你不能在 memoize 函数返回的闭包中使用 vararg 吗?在 Lua 中,你可以做一些事情,比如 t = {...} 将变量参数列表打包到一个表中,或者直接调用一个函数并传递 f(...)。然后只需将 vararg 列表打包为字符串以用作缓存索引。
0赞 Aaron 9/28/2008
注意:如果参数在转换为字符串时包含“,”逗号,这将中断。例如,f(“1”, “2,3”) 的计算结果与 f(“1,2”, “3”) 相同,即使这是不正确的结果。
5赞 Lee Baldwin 9/27/2008 #7

我敢打赌,这样的事情应该适用于 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() 转换参数列表。哦,可能性。

评论

0赞 Jon 'links in bio' Ericson 9/27/2008
干得好!我还没有看过 Lua 中的变量参数列表,所以这是一个很好的例子。
0赞 Aaron 9/28/2008
有没有办法比转换为字符串更有效地将参数转换为值?
1赞 Jon 'links in bio' Ericson 9/28/2008
它可以作为 N 维数组完成,这将解决逗号问题,但缓存访问可能效率较低。数学函数和递归函数是记忆的最佳候选者,所以我认为这些都不是大问题。
1赞 Robert Gould 12/12/2008
您应该添加一个选项,使缓存成为弱表(弱键和弱值),以便缓存可以偶尔清理一次,并避免内存膨胀
1赞 jpjacobs 1/11/2011
在 varg_tostring 函数中使用字符串串联确实是一个性能克星,在每次连接时都会创建新的字符串。更好的做法是简单地使用 table.concat({...},“whateverSeperatorYou'dWant”)。
1赞 Aaron 9/28/2008 #8

本着以不同语言发布记忆的脉络,我想用一个不改变语言的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_;
};

只需创建一个记忆器的实例,向它输入您的函数和参数即可。只要确保不要在两个不同的函数之间共享同一个备忘录(但您可以在同一函数的不同实现之间共享它)。

接下来,一个驱动程序函数和一个实现。只有驱动程序函数需要是公开的 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 输出的永久链接。测量呼叫次数以验证正确性。(在此处插入单元测试...)

这仅记住一个输入函数。对多个参数或不同参数进行概括,作为读者的练习。

9赞 dreeves #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) 好得多。事实上,您甚至可以编造一个场景,其中记忆的实现比封闭形式的实现具有更好的性能!

1赞 Hercynium 11/5/2008 #10

在 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 甚至具有使纪念品缓存持久化的工具,因此不需要在每次调用程序时重新填充它!

文档如下:http://perldoc.perl.org/5.8.8/Memoize.html

2赞 Norman Ramsey 9/7/2010 #11

这是在不将参数转换为字符串的情况下工作的东西。 唯一需要注意的是,它不能处理 nil 参数。但是公认的解决方案无法区分值和字符串,所以这可能没问题。nil"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
2赞 kikito 4/21/2011 #12

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

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
5赞 Amir 11/21/2011 #13

在 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));
0赞 Fractaly 12/6/2011 #14

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

public int triangle(final int n){
   return n * (n - 1) / 2;
}

评论

0赞 Jon 'links in bio' Ericson 12/6/2011
当然,但问题实际上是关于如何编写函数。该函数只是一个示例,用于演示该函数的用法。memoizetriangle
0赞 arviman 4/6/2013 #15

请不要重复这个。要么使用 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];