这类问题 - 评估递归关系 - 多年来已经有很多聪明人研究它,这意味着您可以使用大量很酷的见解和想法来加快速度。
cmets 已经很好地确定了为什么您的代码在大输入时会变慢 - 这是因为您正在生成大量重复的递归调用。那么,问题是如何解决这个问题。
如果您想保持相同的基本策略,我建议您使用memoization。如果您以前没有见过这种技术,基本思想是让递归跟踪已经进行的调用并缓存这些调用的结果。然后,如果您尝试两次解决相同的问题,您可以将缓存的结果交回。
记忆的通用模板看起来像这样。 (它是伪代码,但不应该太难适应。)
def memoized_recursion(original_args, memoization_table):
if memoization_table contains original_args):
return memoization_table[original_args]
else
# Put the rest of your recursive code here.
# Before returning a result, store it in memoization_table.
这极大地减少了递归调用的数量,从而加快了您的代码速度。
当然,这并不是使您的代码更快的唯一解决方案。如果您必须保持递归,则可以使用不同的洞察力从根本上改变策略。基本思路是这样的。您正在生成一系列如下所示的数字:
1, 1, 2, 4, 7, 13, 24, ...
想法是这样的
- 前三项分别为 1、1、2;
- 这个之后的每一项都是前面三个数字的总和;和
- 您想要该系列的第 n 个学期。
如果您需要术语 0、1 或 2,您可以直接阅读答案,因为您知道前三个数字。
如果没有,您可以使用另一种技术。与其获取前面的三个值并将它们相加,不如使用这个有用的事实:要求以 1、1、2 开头的系列的第 n 项等同于要求以 1、1、2 开头的系列的第 (n-1) 项1、2、4。(你明白为什么吗?)
更一般地,如果系列的前三个项是 a、b 和 c,并且您想要第 n 个项,您可以要求从序列 b、c 开始的系列的第 (n-1) 个项, a + b + c。这提供了一种不同的递归策略,其中递归不分支,这意味着您不需要记忆。
现在,最后一个策略。您要解决的问题类型涉及一种称为齐次线性递推关系的东西。也就是说,你有一个重复的形式
- a0、a1、...、ak-1是固定常数,
- an+k = c0 an + c1 an+1 + ... + ck-1an+k-1.
这种重复包括斐波那契数列、佩尔数、帕多万数列等。
事实证明,在任何情况下,如果您要解决这样的递归,您都可以通过将特定选择的矩阵提高到特定的幂来解决问题。在您的情况下,基本思想与第二种递归策略的思想有关。这个想法是,如果序列的最后三个项是 a、b 和 c,那么您知道下一项是 a + b + c,而在这之前的两个项是 b 和 c。换句话说,您可以想象一个将 (a, b, c) 转换为 (b, c, a + b + c) 的映射。这可以被认为是这个矩阵方程:
| 0 1 0 | |a| | b |
| 0 0 1 | |b| = | c |
| 1 1 1 | |c| | a + b + c |
如果你让 M 是最左边的矩阵,那么计算 Mn 并将其乘以列向量 (a, b, c) 将得到第 n、(n+1)st 和 (n+2) )nd 递归关系的项。这给出了解决问题的完全不同的策略:构建一个矩阵,然后将其提升到一个大幂!
事实上,您可以非常有效地做到这一点。有一种称为exponentiation by squaring 的(递归)技术可以仅使用 O(log n) 乘法来计算矩阵的 n 次方。 (不幸的是,矩阵的条目将开始变得非常大,并且将它们相乘将开始成为您的瓶颈)。不过,这个策略可能值得一试,因为它是一种非常酷的技术!
最后,还有最后一个选项。如果您进行一些谷歌搜索,您会发现您的问题与找到第 n 个tribonacci number 密切相关。您可以使用一些很酷的公式来直接计算它,也涉及数字的幂,尽管它们可能会引入一些舍入错误,从而对您的目的来说太慢了。