【问题标题】:Generate a recursive series faster (similar to Fibonacci)更快地生成递归序列(类似于斐波那契)
【发布时间】:2021-08-04 16:02:34
【问题描述】:

你必须计算一个序列的第 n 项,因为 n 非常大,并且必须尽可能快地完成。

系列由以下函数定义:

f(0) = 1
f(1) = 1
f(2n) = f(n)
f(2n+1) = f(n) + f(n-1)

我知道我必须使用记忆。我做了这段代码,但问题是大 n 值会导致分段错误。我想尝试做一个 2 值数组版本(如回复 here 中描述的那个),但我没有找到正确的解决方案。

uint64_t f(uint64_t n)
{
    uint64_t a[n+2];
    uint64_t i;

    a[0] = 1;
    a[1] = 1;
 
    for(i = 2; i <= n; i++)
    {
        if (i % 2 == 0)
        { 
            a[i] =  a[i / 2];
        }
        else
        {
            a[i] =  a[i / 2] + a[(i / 2) - 1];
        }
    }
    return a[n];
};

【问题讨论】:

  • n 的什么值导致设置错误?
  • Variable-Length Arrayuint64_t a[n+2]; 不在标准 C++ 中。
  • 我也尝试过使用 unsigned long long。值大于 100000 左右时会发生分段错误。
  • 使用std::vector&lt;uint64_t&gt; a(n+2); 而不是uint64_t a[n+2];
  • 向量有帮助,但是当 n 真的很大时,代码也会死掉。

标签: c++ series fibonacci


【解决方案1】:

我认为递归在这里是正确的想法,因为您只需要最后一个值,它实际上并不依赖于那么多先前的值。最好的情况是,如果您的输入是 2 的幂,例如 2^n。然后你只需要 n 个输入的值。 尽管在其他情况下性能更差,但它仍然应该比实际计算所有前面的值要好得多。

编辑:使用 cmets 中请求的特定数字和一个计数器变量来显示所需的评估次数:

编辑2: 当然,将递归与缓存中间结果结合起来是可能的(并且是真正的性能提升!),正如@Bob__ 在下面的评论中所展示的那样,谢谢! 对于未来的读者,这里有递归+缓存的完整版本。在给定输入的情况下,缓存将 g 所需的求值次数从没有缓存的 12875760616 显着减少到只有 164 个缓存。

#include <iostream>
#include <map>
#include <stdexcept>

static unsigned long count = 0;
static std::map<unsigned long, unsigned long> cache;

static unsigned long g(unsigned long n) {
  auto it = cache.find(n);
  if (it != cache.end()) {
    return it->second;
  }

  ++count;
  if (count == 0) {
    std::cout << "Integer overflow! Aborting!";
    throw std::overflow_error("overflow error!");
  }

  if (n == 0 or n == 1) {
    return 1;
  } else if (n % 2 == 0) {
    auto a = g(n / 2);
    cache.insert({n, a});
    return a;
  } else {
    auto a = g((n - 1) / 2) + g((n - 3) / 2);
    cache.insert({n, a});
    return a;
  }
}

int main() {
  try {
    std::cout << "result: " << g(123456789012345678) << std::endl;
    std::cout << "number of used values: " << count << std::endl;
  } catch (std::exception &e) {
    std::cout << "An error occured:\n" << e.what() << std::endl;
  }
}

【讨论】:

  • 感谢艾克的帮助。但是,我认为递归解决方案并不是最有效的解决方案。
  • 视情况而定。您只需要最后一个值,例如您的函数返回,还是需要从 1 到 n 的所有值?在第一种情况下,我有理由确定,递归是最好的解决方案,因为我在回答开始时就考虑到了。
  • @A.Palomo 你认为 ;) 还是你测量过? n 在您的测试用例中可以有多大,您必须执行多少测试?
  • 嗨,艾克。最后,感谢您的帮助,我想我会坚持使用递归版本。我确信这是最幼稚和最慢的版本,我不得不使用动态编程或记忆。但目标是产生一个在毫秒范围内工作的解决方案,而递归选项可以做到这一点。再次感谢您的帮助。
  • @A.Palomo 请注意,记忆化会产生巨大的影响(参见例如godbolt.org/z/TxY1ahaKc),尤其是在这些类型的递归算法中,return g(...) + g(...) 之类的语句会强制程序重新计算许多值。
【解决方案2】:

我们可以一次考虑两组四个值来扩展发布的重复关系。

请注意,这些集合的第一个元素始终位于偶数位置(当 n = 2k+1 时为 2n,n-1 或当 n = 2k 时为 n-2),并且每个集合中只有三个元素位于较低的位置职位实际参与计算。

从给定的 N 开始,我们要为其计算 FN,我们可以找到一系列递减的值 ni 代表这些位置和 p i 指示要使用的转换。

n0 = N - N mod 4 hi = ni / 2 pi = hi mod 2 ni+1 = hi + pi - 2

那么我们只需要将前面的变换连续应用到向量{F0, F1, F2, F3} = {1, 1, 1, 2} 我们可以在 O(logN) 中找到 FN

以下是一种可能的实现方式

unsigned long long f(unsigned long long n)
{
    unsigned long long F[] = {
        1, 1, 1, 2
    };
    auto idx = n % 4;
    auto pos = n - idx;
    unsigned long long journal{};
    int count{};
    while ( pos )
    {
        auto n = pos / 2;
        journal <<= 1;
        journal |= n % 2;
        pos = n + n % 2 - 2;
        ++count;
    }
    while ( count-- )
    {
        if ( journal & 1u )
        {
            unsigned long long tmp = F[1];
            F[1] += F[0];
            F[0] = tmp;
            F[3] = F[2] + tmp;
        }
        else
        {
            unsigned long long tmp = F[2];
            F[0] = tmp;
            F[1] += tmp;
            F[2] = F[3];
            F[3] += tmp;
        }
        journal >>= 1;
    }
    return F[idx];
}

可测试here

【讨论】:

  • 非常酷!你在这方面做了更多的理论。您认为甚至可以创建一个明确的公式吗?我的第一印象是“不”,但这些矩阵乘积至少看起来更像斐波那契数列的显式公式推导。
  • @Eike 好吧,这超出了我的知识范围。当然,我在考虑斐波那契数列的矩阵表示,但在这种情况下,有 一个 矩阵,它很容易可指数化,并且有几个很好的特征值......
猜你喜欢
  • 1970-01-01
  • 2011-03-21
  • 2012-11-29
  • 2010-12-03
  • 2011-07-27
  • 2023-04-06
  • 2014-04-02
  • 2012-11-19
相关资源
最近更新 更多