嘿。这个例子非常具体,但我认为它可以适用于广泛的功能。 它来自一些在线编程竞赛。
有一个简单获胜条件的游戏。画画是不可能的。游戏不能永远持续,因为每一步都会让你更接近终止条件。在给定状态的情况下,该功能应确定现在要移动的玩家是否具有获胜策略。 在该示例中,状态是整数。玩家选择一个非零数字并从数字中减去它:新状态是新的整数。获胜者是达到零的玩家。
我编码了这个:
from Memoize import Memoize
@Memoize
def Game(x):
if x == 0: return True
for digit in str(x):
if digit != '0' and not Game(x-int(digit)):
return True
return False
我认为很明显它是如何运作的。我也意识到,对于这个特定的游戏,可能有一个更智能的解决方案,但我的问题是一般的。然而,即使对于相对较小的输入,这也使得python变得疯狂。有没有办法让这个代码与循环一起工作?
由于
这就是我翻译成循环的意思:
def fac(x):
if x <= 1: return x
else: return x*fac(x-1)
def fac_loop(x):
result = 1
for i in xrange(1,x+1):
result *= i
return result
## dont try: fac(10000)
print fac_loop(10000) % 100 ## works
答案 0 :(得分:4)
通常,只有当递归函数为primitive-recursive时,才能将它们转换为循环;这基本上意味着他们只在身体中称自己一次。您的函数多次调用自身。这样的功能确实需要堆栈。可以使堆栈明确,例如与列表。使用显式堆栈重新算法算法是
def Game(x):
# x, str(x), position
stack = [(x,str(x),0)]
# return value
res = None
while stack:
if res is not None:
# we have a return value
if not res:
stack.pop()
res = True
continue
# res is True, continue to search
res = None
x, s, pos = stack.pop()
if x == 0:
res = True
continue
if pos == len(s):
# end of loop, return False
res = False
continue
stack.append((x,s,pos+1))
digit = s[pos]
if digit == '0':
continue
x -= int(digit)
# recurse, starting with position 0
stack.append((x,str(x),0))
return res
基本上,您需要使每个局部变量成为堆栈帧的元素;这里的局部变量是x,str(x)和循环的迭代计数器。执行返回值有点棘手 - 如果函数刚刚返回,我选择将res设置为not-None。
答案 1 :(得分:3)
通过“疯狂”我认为你的意思是:
>>> Game(10000)
# stuff skipped
RuntimeError: maximum recursion depth exceeded in cmp
你可以从底部开始 - 粗略的改变是:
# after defining Game()
for i in range(10000):
Game(i)
# Now this will work:
print Game(10000)
这是因为,如果你从较高的数字开始,你必须在到达底部(0)之前进行很长的路径,所以你的memoization装饰器不会有所帮助。
从底部开始,确保每个递归调用立即触及结果字典。你可能会使用额外的空间,但是你没有进行过多的推算。
您可以通过使用循环和堆栈将任何递归函数转换为迭代函数 - 实际上是手动运行调用堆栈。有关讨论,请参阅this question或this quesstion。这里可能有一个更优雅的基于循环的解决方案,但它并没有向我跳跃。
答案 2 :(得分:0)
好吧,递归主要是关于能够执行一些代码而不会丢失先前的上下文及其顺序。特别是,函数帧在递归期间放入并保存到调用堆栈中,因此对递归深度给出约束,因为堆栈大小有限。您可以通过在堆内存上创建状态堆栈来手动管理/保存每个递归调用所需的信息,从而“增加”递归深度。通常,可用堆内存量大于堆栈内存量。想一想:良好的快速排序实现通过创建具有不断变化的状态变量的外部循环(QS示例中的下/上数组边界和枢轴)来消除向较大侧的递归。
当我输入此内容时,Martinv.Löwis发布了关于将递归函数转换为循环的良好答案。
答案 3 :(得分:0)
您可以稍微修改递归版本:
def Game(x):
if x == 0: return True
s = set(digit for digit in str(x) if digit != '0')
return any(not Game(x-int(digit)) for digit in s)
这样,您不会多次检查数字。例如,如果你正在做111,你不必三次看110.
我不确定这是否算作您所提出的原始算法的迭代版本,但这是一个记忆的迭代版本:
import Queue
def Game2(x):
memo = {}
memo[0] = True
calc_dep = {}
must_calc = Queue.Queue()
must_calc.put(x)
while not must_calc.empty():
n = must_calc.get()
if n and n not in calc_dep:
s = set(int(c) for c in str(n) if c != '0')
elems = [n - digit for digit in s]
calc_dep[n] = elems
for new_elem in elems:
if new_elem not in calc_dep:
must_calc.put(new_elem)
for k in sorted(calc_dep.keys()):
v = calc_dep[k]
#print k, v
memo[k] = any(not memo[i] for i in v)
return memo[x]
它首先计算输入所依赖的x的数字集合。然后它计算这些数字,从底部开始并向x方向移动。
由于对calc_dep的测试,代码非常快。它避免了计算多个依赖项。结果,它可以在400毫秒内完成游戏(10000)而原始游戏 - 我不知道多久。很长一段时间。
以下是性能测量:
Elapsed: 1000 0:00:00.029000
Elapsed: 2000 0:00:00.044000
Elapsed: 4000 0:00:00.086000
Elapsed: 8000 0:00:00.197000
Elapsed: 16000 0:00:00.461000
Elapsed: 32000 0:00:00.969000
Elapsed: 64000 0:00:01.774000
Elapsed: 128000 0:00:03.708000
Elapsed: 256000 0:00:07.951000
Elapsed: 512000 0:00:19.148000
Elapsed: 1024000 0:00:34.960000
Elapsed: 2048000 0:01:17.960000
Elapsed: 4096000 0:02:55.013000
这很合理。