【问题标题】:Memoized recursive functions. How to make them fool-proof?记忆递归函数。如何让他们万无一失?
【发布时间】:2011-09-14 10:13:50
【问题描述】:

记忆函数是记住它们找到的值的函数。 如有必要,请在文档中心查看有关 Mathematica 的一些背景信息。

假设你有以下定义

f[0] = f[1] = 1
f[x_] := f[x] = f[x - 1] + f[x - 2]

在您的一个包裹中。用户可以加载包并立即开始询问 f[1000]。 这将触发 $RecursionLimit::reclim 错误消息并中止。 即使用户然后尝试更小的东西,比如 f[20],到现在 f 的定义已经损坏并且结果不再好。当然包开发人员可能会增加递归限制并警告用户,但我的问题是:

如何改进 f 的定义,以便在用户询问 f[1000] 时,他/她可以毫无问题地得到答案?我对捕获用户输入的方法感兴趣,对其进行分析并采取任何必要的步骤来评估 f[1000]。

我可以很容易地想象如果输入超过 255 可以更改递归限制(然后将其恢复到原始级别),但我真正想看到的是,如果有办法f 找出它“知道”多少个值(fknownvalues)并接受任何输入

感谢您的帮助

【问题讨论】:

  • 这在一般情况下看起来很棘手,特别是如果您有多个参数,并且递归不会在每一步中严格减少它们。
  • @Thilo 如果不是很棘手,我不会问它:-)

标签: wolfram-mathematica


【解决方案1】:

以下代码假设您可以从输入参数的值中确定 $RecursionLimit 的值:

Clear[f];
Module[{ff},
  ff[0] = ff[1] = 1;
  ff[x_] := ff[x] = ff[x - 1] + ff[x - 2];

  f[x_Integer] :=f[x] =
     Block[{$RecursionLimit = x + 5},
        ff[x]
  ]]

我正在使用本地函数 ff 来完成主要工作,而 f 只是将其封装在 Block 中,并为 $RecursionLimit 设置适当的值:

In[1552]:= f[1000]
Out[1552]=  7033036771142281582183525487718354977018126983635873274260490508715453711819693357974224
9494562611733487750449241765991088186363265450223647106012053374121273867339111198139373125
598767690091902245245323403501  

编辑

如果你想对$RecursionLimit的设置更精确,可以将上面部分代码修改为:

f[x_Integer] :=
  f[x] =
    Block[{$RecursionLimit = x - Length[DownValues[ff]] + 10},
    Print["Current $RecursionLimit: ", $RecursionLimit];
    ff[x]]]

Print 声明用于说明。 10 的值相当随意——要获得它的下限,必须计算必要的递归深度,并考虑到已知结果的数量是 Length[DownValues[ff]] - 2(因为 ff 有 2 个通用定义)。下面是一些用法:

In[1567]:= f[500]//Short

During evaluation of In[1567]:= Current $RecursionLimit: 507
Out[1567]//Short= 22559151616193633087251269<<53>>83405015987052796968498626

In[1568]:= f[800]//Short

During evaluation of In[1568]:= Current $RecursionLimit: 308
Out[1568]//Short= 11210238130165701975392213<<116>>44406006693244742562963426

如果您还想限制可能的最大$RecursionLimit,这也很容易做到,按照相同的思路。例如,在这里,我们将其限制为 10000(同样,这在 Module 内部):

f::tooLarge = 
"The parameter value `1` is too large for single recursive step. \
Try building the result incrementally";
f[x_Integer] :=
   With[{reclim = x - Length[DownValues[ff]] + 10},
     (f[x] =
        Block[{$RecursionLimit = reclim },
        Print["Current $RecursionLimit: ", $RecursionLimit];
        ff[x]]) /; reclim < 10000];

f[x_Integer] := "" /; Message[f::tooLarge, x]]

例如:

In[1581]:= f[11000]//Short

During evaluation of In[1581]:= f::tooLarge: The parameter value 11000 is too 
large for single recursive step. Try building the result incrementally
Out[1581]//Short= f[11000]

In[1582]:= 
f[9000];
f[11000]//Short

During evaluation of In[1582]:= Current $RecursionLimit: 9007
During evaluation of In[1582]:= Current $RecursionLimit: 2008
Out[1583]//Short= 5291092912053548874786829<<2248>>91481844337702018068766626

【讨论】:

  • 这是@magma 可以“轻松想象”的部分。考虑到已经有一些记忆值,如果有一种方法可以确定 $RecursionLimit 确实需要提高多远。
  • @Thilo 请查看我的编辑 - 基于 magma 的建议(假设递归深度为 input - number-of-known-results + const。如果它仍然超过该值,我的最后一个代码有一个限制,它将告诉用户计算必须是分成几个步骤。
  • 既然$RecursionLimit 只在本地设置了这个函数,为什么不在Block 中设置为Infinity 而不是想出一个“足够大”的值呢?无论如何,安全问题仍然存在:递归太深,内核会崩溃。我不知道有什么方法可以确定最大的碰撞安全$RecursionLimit,如果有人知道,请告诉我。
  • @Szabolcs 我猜$RecursionLimit 没有单一的通用限制“安全”值,因为它必须由堆栈使用的内存决定,这取决于问题。我的实验导致$RecursionLimit 崩溃了数十万。可能应该可以估计可用堆栈空间,尽管这不会有太大帮助。无论如何,我永远不会使用$RecursionLimit = Infinity,因为这是真正的灾难秘诀。如果可能,应该使用尾递归(在 mma 意义上)函数来减少递归到迭代。
  • @magma 请注意,对于大量(数万或更多)定义,DownValues[f] 可能需要相当长的时间来执行。如果这种情况经常发生,您可能需要保留一个单独的计数器(本地化在同一个 Module 内),每次添加新定义时都会递增,以减少开销。
【解决方案2】:

对 Leonid 的代码稍作修改。我想我应该把它作为评论发布,但是评论格式的缺乏使得它不可能。

自适应递归限制

Clear[f];
$RecursionLimit = 20;
Module[{ff},
 ff[0] = ff[1] = 1;
 ff[x_] := 
  ff[x] = Block[{$RecursionLimit = $RecursionLimit + 2},  ff[x - 1] + ff[x - 2]];
 f[x_Integer] := f[x] = ff[x]]

f[30]
(*
-> 1346269
*)

$RecursionLimit
(*
-> 20
*)

编辑

尝试稀疏设置 $RecursionLimit:

Clear[f];
$RecursionLimit = 20;
Module[{ff}, ff[0] = ff[1] = 1;
 ff[x_] := ff[x] =
   Block[{$RecursionLimit =
      If[Length@Stack[] > $RecursionLimit - 5, $RecursionLimit + 5, $RecursionLimit]}, 
       ff[x - 1] + ff[x - 2]];
 f[x_Integer] := f[x] = ff[x]]  

不确定它有多大用处......

【讨论】:

  • +1。我正在考虑添加与我的答案类似的内容,但是您首先做到了,而且可能比我打算做的更优雅。如果小步增加的开销太大,可能需要增加$RecursionLimit 步骤。
  • @Leonid 我猜粗略的步进控制需要增加一个变量,或者至少测量堆栈深度。
  • 也许你是对的。这个建议我没想太多,实施起来可能比较难。
  • 这也是一个很有意思的解决方案,使用Block inside Block递归
猜你喜欢
  • 2014-10-31
  • 2021-10-23
  • 2010-09-20
  • 2012-11-26
  • 1970-01-01
  • 2012-04-01
  • 1970-01-01
  • 1970-01-01
  • 2020-10-21
相关资源
最近更新 更多