【问题标题】:Python understanding Max Recursion Depth exceeded (Dynamic Programming)Python 理解 Max Recursion Depth exceeded (Dynamic Programming)
【发布时间】:2022-01-10 16:33:14
【问题描述】:

我正在重新审视一些动态编程概念,并编写了一个代码来计算带有记忆的斐波那契。

代码如下:

def fib(n,memo={}):
    if(n in memo):
        return memo[n]
    if(n <= 2):
        return 1
    memo[n]=fib(n-1,memo) + fib(n-2,memo)
    return memo[n]

现在我运行了一些测试用例,结果如下

>>> fib(2)
1
>>> fib(1000)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in fib
  File "<stdin>", line 6, in fib
  File "<stdin>", line 6, in fib
  [Previous line repeated 995 more times]
  File "<stdin>", line 4, in fib
RecursionError: maximum recursion depth exceeded in comparison
>>> fib(6)
8
>>> fib(10)
55
>>> fib(100)
354224848179261915075
.
.
.
.
>>> fib(980)
2873442049110331124686847975839596483580184681281842823699466692000268066325404550898791458706068536914736664630537864515125212890415097163803163111745199726085365105
>>> fib(990)
3534100091787525753399448335204590682849450463581549776041091752538906966342713601215835661100647255108360758515849851434123968685864251091027232911065706187500753920
>>> fib(999)
2686381002448535938614672720214292396761660931898695234012317599761798170024788168933836965448335656419182785616144335631297667364221035032463485041037768036733415116
>>> fib(1000)
4346655768693745643568852767504062580256466051737178040248172908953655541794905189040387984007925516929592259308032263477520968962323987332247116164299644090653318795

当我第一次运行 fib(1000) 时,它说超出了最大递归深度。但是,当我逐渐增加 n 时,fib(1000) 工作正常。 然后我尝试了 fib(2000) 并得到了同样的异常。

>>> fib(2000)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in fib
  File "<stdin>", line 6, in fib
  File "<stdin>", line 6, in fib
  [Previous line repeated 995 more times]
  File "<stdin>", line 4, in fib
RecursionError: maximum recursion depth exceeded in comparison

我尝试逐渐增加 n 并且它再次正常工作:

>>> fib(1200)
2726988445540627015799161531364219870500077999291772582118050289497472647637302680948250928456231003117017238012762721449359761674385644301603997220584740591763466070
>>> fib(1500)
1355112566856310195163693686714840837778601071241849724213354315322148731087352875061225935403571726530037377881434732025769925708235655004534991410292424959599748390
>>> fib(1700)
8501653935514177120246392248625833924052052390491381030300605977750345588982825628424071479174753549360050542305550855066813804919653208931716726270523366654632196915
>>> fib(1900)
5333735470177196739708654380013216364182711606231750028692155598599810955874132791398352277818697705852238294681640540003099177608752396895596802978549351480795061055
>>> fib(2000)
4224696333392304878706725602341482782579852840250681098010280137314308584370130707224123599639141511088446087538909603607640194711643596029271983312598737326253555805
>>> fib(2500)
1317090516751949629522763087125316412066606964992507141887746936727530870405038425764503130123186407746570862185871925952766836352119119528156315582632460790383834605
>>> fib(2900)
5184080332847202181832545365520373859688699234105705045492742368770388504951261158081878962852500283133276036303031796698449718008155302155556519351587134410081144235
>>> fib(3000)
4106158863079712603335683787192671052201251086373692524088854309269055842741134037313304916608500445608300368357069422745885693621454765026743730454468521604866062920

如果我之后立即运行 fib(4000) 也会发生同样的事情,但如果我逐渐增加它会正常工作。我基本上试图理解为什么会这样。 memo 对象不是全局的,应该在第一次调用该函数时进行初始化,因此将 n 连续增加到 1000 理论上应该与直接调用 fib(1000) 没有什么不同。

【问题讨论】:

  • 在调用fib函数之前尝试运行memo={}

标签: python dynamic-programming fibonacci memoization


【解决方案1】:

这是因为如果memo 为空,则递归需要一直到n &lt;= 2 的基本情况。因此,如果您立即开始第一次调用 fib(1000),您可能会遇到堆栈溢出。

但是,当您从较小的值开始时,例如调用 fib(10)memo 会收集很多结果,包括 109。所以下一次你打电话增加你传递的参数时,它不必一直重复到 2,但当它达到 9 或 10 时已经可以回溯,因为它会发现它已经在 @ 987654328@.

请注意,memo 仅在函数被定义时初始化为 {},因此您只需继续扩展它,从而减少使用调用堆栈进行深度递归的需要。

【讨论】:

  • 啊,你的最后一句话回答了我的问题。在我看来,每次调用 fib(1000) 之类的函数时,memo 都应该初始化,递归调用将使用该 memo 对象,但是当我调用 fib(2000) 时,memo 应该在第一次调用期间再次初始化。你的回答很有道理。非常感谢!
  • 是的,这在 Python 中很奇怪。在大多数其他流行语言中,默认参数在 调用时进行评估。
  • @KhizarAmin 为什么您希望它在“第一次通话”中被重置?它没有被重置的事实是记忆首先起作用的全部原因:因为memo 在函数调用之间持续并保持其状态。 Python 不区分您在 shell 中显式调用的函数调用和作为源代码一部分的递归函数调用。如果你每次都启动一个新的 shell,你会得到不同的结果。
  • @PaulM.,请注意,在代码中递归调用显式传递memo。这源于它在许多其他语言中的工作方式。在 Python 中,您甚至不必传递参数,因为该参数的默认值不会以任何一种方式计算(无论您是否传递参数)。这可能会让一些人感到困惑,尤其是当这种行为在其他语言中有所不同时。
  • @PaulM。那是因为第一个电话是我打的,其中 memo 是一个空字典。对于递归调用,使用相同的备忘录对象。这对我来说很有意义。然而,一旦递归调用完成并且函数返回,并且我再次调用 fib(1000),我认为 memo 对象会再次初始化,但事实证明并非如此(参考 trincot 的评论)。跨度>
【解决方案2】:

正如 trincot 所提到的,没有数据存在,所以代码会去寻找另一个对函数的调用而不是值,直到该值在堆栈限制内是可计算的。理论上,对函数的调用会在某处(堆栈)保留一些空间,其中存在函数的返回点、func 的参数,也许还有其他东西。发生在你身上的是,当备忘录为空并且你还没有开始任何计算时,你重复存储或返回点 + args + 其他东西 N 次,因此唯一的输出是递归耗尽或存储(堆栈)用尽,取决于两种尺寸。

但是,您会遇到一个很好的异常,所以当这种情况发生时,只需捕获它,将数字分成几部分(例如除以二/四/...)并再次返回(递归)。这样,即使你用递归调用破坏堆栈,你也会递归地找到一个较小的数字来填充备忘录,然后你慢慢地引导你找到更大的数字。

【讨论】:

  • 这是个好主意,我试图了解 python 如何创建/处理备忘录对象。我以为每次调用 fib 时都会创建它,然后用于递归调用。拆分成部分的建议很整洁,我也会添加。
  • @KhizarAmin 让自己扮演代码解析器的角色。何时创建/准备好使用模块?一旦一切都被解析。一个模块由函数(和其他东西)组成。 func 什么时候准备好?一旦其代码可读(尚未执行)并创建其属性(参数、名称等)。 func 的“属性”值是原始值,例如 int(不可变)或对象,例如 dict(可变)。一旦它为函数创建,它在函数的生命周期内是全局的。对于可变对象,这意味着内容(例如计算的 fib)通过递归保留,有点像半全局值。
  • 是的,我明白了。但是一旦函数完成执行并返回我说 fib(5) 的结果,我期待它为 fib(10) 的顶级调用创建一个新的备忘录对象,但 python 处理它的方式与前面提到的其他语言不同通过 trincot
  • @KhizarAmin 是的,这就是您看到的半全局状态。尝试运行print(id(memo)) vs id(fib.__defaults__[idx]),你会看到。
猜你喜欢
  • 1970-01-01
  • 2013-03-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多