重构左递归死循环

Refactor left recursion infinite loop

当你尝试实现这个公式时

直接使用此代码

def P(n, p, limit):
    if n == limit:
        return 1

    if n == -limit:
        return 0

    return p*P(n+1, p, limit) + (1-p)*P(n-1, p, limit)

你肯定会遇到这个错误。

RecursionError: maximum recursion depth exceeded

那么如何正确地使用代码实现这样的class问题呢?有没有一种技术可以用来重构它并获得解决方案?或者唯一的办法就是先用数学方法解决这个问题,然后再输入计算?

递归取决于将问题分解成多个部分,每个部分都更接近于一个基本情况。您给出的公式是无限的,因为它取决于同样未定义的级数项。看似简单的数学符号并不能保证可计算性。

在这种情况下,您必须以非循环方式重写递归关系。你仍然可以使用递归,但必须有一个明确的结束。在给定的公式中,P[n]依赖于P[n+1]和P[n-1],每个直接依赖于P[n]。这是算法循环。

您需要从基本案例中提取算法,从末端开始,逐步深入。此递归关系的不幸结果之一是您必须求解整个范围 [-limit, limit]为了得到你想要的职位的价值。基本案例值从两端波动到您的观点。

没有工具可以象征性地为您解开这个谜团。最重要的是,请注意您的基本情况不会出现在等式中。即使您将这些作为附加方程式提供,从这种关系中导出指数级数也超出了当今求解器的分析能力。

为了扩展 @Prune 的建议,通过将 RHS 上的任一术语移动到 LHS,可以将此循环写成 两个 其他循环:

这些是非循环的,因为每个项仅取决于 之前的项,而不是两者。


然而,它们不能独立求解,因为这需要 P1-l 和 Pl-1 的知识,其中 l = limit.

为方便起见做一些替换:

请注意,由于关系仅包含 线性 项(没有例如 Pn × Pn-1 等),其展开式中的所有项在变量 x, y:

中也将是线性的

这些新引入的系数也服从类似的递归关系:

它们的边界条件是:

现在已知边界条件,可以迭代计算系数。剩下的只是将 P1-l 和 Pl-1 的表达式从两个递归中等同起来:

求解这对联立方程得到P1-l和Pl-1,从中任意Pn可以计算(使用之前计算的系数):


更新:样本Python实施

# solve a recurrence of the form
# Tn = c1 * Tn-1 + c2 * Tn-2, n >= 0
def solve_recurrence(c1, c2, t1, t2, n):
    if n <= 0: return t2
    if n == 1: return t1
    a, b = t2, t1
    for i in range(1, n):
        a, b = b, c1 * b + c2 * a
    return b

# solve the loop recurrence
def solve_loop(limit, p, n):
    assert(limit > 0 and abs(n) <= limit)
    assert(p > 0 and p < 1)

    # corner cases
    if n == -limit: return 0
    if n ==  limit: return 1

    # constants
    a, c = 1 / p, 1 / (1 - p)
    b, d = 1 - a, 1 - c

    # coefficients at 2l - 1
    e = 2 * limit - 1
    l = solve_recurrence(a, b, 1, 0, e)
    m = solve_recurrence(a, b, 0, 0, e)
    s = solve_recurrence(c, d, 1, 0, e)
    t = solve_recurrence(c, d, 0, 1, e)
    # note that m can just be 0 but was left in for generality

    # solving the simultaneous equations
    # to get P at 1 - l and l - 1
    x = (s * m + t) / (1 - s * l)
    y = (l * t + m) / (1 - l * s)

    # iterate from the closest side to improve precision
    if n < 0:
        return solve_recurrence(a, b, x, 0, limit + n)
    else:
        return solve_recurrence(c, d, y, 1, limit - n)

limit = 10, p = 0.1 的实验结果:

n       | P(n)            abs. error
========================================
-10     | 0.0000000E+00   --
-9      | 6.5802107E-19   0.0000000E+00
-8      | 6.5802107E-18   1.7995889E-30
-7      | 5.9879917E-17   1.7995889E-29
-6      | 5.3957728E-16   5.0003921E-28
-5      | 4.8568535E-15   8.1994202E-27
-4      | 4.3712339E-14   8.9993252E-27
-3      | 3.9341171E-13   5.1002066E-25
-2      | 3.5407061E-12   5.9938283E-25
-1      | 3.1866355E-11   5.9339980E-18
 0      | 2.8679726E-10   5.3406100E-17
 1      | 2.5811749E-09   3.3000371E-21
 2      | 2.3230573E-08   2.6999175E-20
 3      | 2.0907516E-07   2.2999591E-19
 4      | 1.8816764E-06   7.9981086E-19
 5      | 1.6935088E-05   1.9989978E-18
 6      | 1.5241579E-04   3.5000757E-16
 7      | 1.3717421E-03   1.5998487E-15
 8      | 1.2345679E-02   3.2000444E-14
 9      | 1.1111111E-01   6.9999562E-14
 10     | 1.0000000E+00   --

备注:

  • 从代码和方程式可以清楚地看出,递归限制/边界条件可以是任意的。
  • 可以使用 Kahan-Neumaier 等求和技术提高数值精度。
  • 对于 solve_recurrence 中使用的迭代方法,还有一个替代显式公式,如果与库函数结合使用,可能会产生更准确的结果。

我有另一个使用符号计算的解决方案。

class X(object):
    """Representing unknown value which will be resolved in future"""
    def __init__(self, value=1.0):
        self.value = value

    def __add__(self, other):
        cls = self.__class__
        return cls(value=self.value+other.value)

    def __sub__(self, other):
        cls = self.__class__
        return cls(value=self.value-other.value)

    def __mul__(self, other):
        cls = self.__class__
        if isinstance(other, cls):
            return cls(value=self.value*other.value)
        elif isinstance(other, numbers.Real):
            return cls(value=self.value*other)
        else:
            raise TypeError(f'Cannot multiply with type {type(other)}')

    def __rmul__(self, other):
        return self.__mul__(other)

我们没有立即获得数字结果,而是依赖于这样一个事实,即结果值将以某种方式表示为

每个中间项将表示为

这就是我们为对象定义 add/subtract/multiplicate 操作的原因。

所以在代码中有这样一个符号的表示,我们可以用类似于公认答案的方法来求解这个方程(我们从 -limitlimit 两边出发,并将其他项表示为未知产品)。当到达基点(0 状态)时,我们将得到概率与该比率的数值之比,这使我们可以轻松计算结果

_memory = []
def fill_memory(n):
    global _memory
    _memory = [(0, X(1))]*(2*n+1)
    _memory[-n] = (0, X(0))
    _memory[n] = (1, X(0))


def resolve(n, p):
    q = 1-p
    for i in reversed(range(n)):
        # resolve from right to center
        prob_next = _memory[i+1]
        prob_prev = _memory[i-i]
        _memory[i] = (
            q*prob_prev[0] + p*prob_next[0],
            q*prob_prev[1] + p*prob_next[1],
        )

        # resolve from left to center
        prob_prev = _memory[-i-1]
        prob_next = _memory[-i+1]
        _memory[-i] = (
            q*prob_prev[0] + p*prob_next[0],
            q*prob_prev[1] + p*prob_next[1],
        )

    # calculate base probability
    solution = _memory[0]
    coef = (X(value=1) - solution[1]).value
    p0 = solution[0] / coef
    return p0

我用这种方法获得了正确且非常精确的结果(我使用的是 p=0.6N=4(限制))

def main():
    N = 4
    p = 0.6
    fill_memory(N)
    p0 = resolve(N, p)
    print(f'Probability for N={N} steps is {p0*100:.4}%')
    for i in range(2*N+1):
        print(i-N, '->', _memory[i-N])
4 - 83.505%
3 - 77.143%
2 - 69.231%
1 - 60.0%

如您所见,这与数学求解的结果相同。