【问题标题】:Calculate the number of representations of a number as a sum of fibonacci numbers将数字的表示数量计算为斐波那契数的总和
【发布时间】:2014-04-05 01:55:05
【问题描述】:

我的团队努力寻找一个好的算法,但我们只能想出一个指数算法。有没有办法让它更快?这是完整的问题:

定义一个函数

function F(n:Integer):Integer;

它将计算非负数 n 的不同表示的数量,作为斐波那契数与不相等的正索引的总和。例如(Fib(k) 表示第 k 个斐波那契数):

F(0)=0

F(1)=2,因为 1=Fib(1)=Fib(2)

F(2)=2,因为 2=Fib(3)=Fib(1)+Fib(2)

F(3)=3,因为 3=Fib(4)=Fib(3)+Fib(1)=Fib(3)+Fib(2)

等等

我认为第一步,不可避免的步骤是制作一个 n 个斐波那契数的数组,如下所示:

Fib[1]:=1;
Fib[2]:=1;
for i:=3 to n do
    Fib[i]:=Fib[i-1]+Fib[i-2];

当然,我们可以通过只计算那些小于或等于 n 的斐波那契数来优化它,但这并没有多大帮助,因为无论如何都不允许使用动态数组。那么我们如何才能避免指数时间复杂度呢?

【问题讨论】:

  • 我会说 F(0) = 1(空和)
  • 作弊(但仍然很有教育意义)的方法是找出前 15 个左右整数的答案,然后在整数序列在线百科全书中查找序列。对于这种特殊情况,这给出了oeis.org/A000121。我同意@NiklasB。关于F(0),顺便说一句:0 可以表示为空和。
  • @user2566092:是的,是的。它甚至是指数空间,因为您最终会得到一个(非稀疏)幂级数,其中 O(n) 项作为中间结果。 (顺便说一句,什么序言?我只看到 Maple 和 GP/Pari。)
  • @EgorSkriptunoff:哇,这已经是最明确的了:D

标签: algorithm time-complexity fibonacci


【解决方案1】:

让我们证明一些关于数字的东西:

引理 1:令 n ≥ 1 为整数,Fib(i) 为具有 Fib( i) ≤ n。然后在将 n 表示为具有不同索引的斐波那契数的总和时,出现 Fib(i)Fib(i - 1) ,但不是两者兼而有之。

证明: 我们可以通过归纳证明和 Fib(1) + Fib(2) + ... + Fib(i - 2) = Fib(i) - 1 。由于 Fib(i) ,我们在表示中至少需要 Fib(i - 1)Fib(i)。但不是两者都有,因为 Fib(i) + Fib(i - 1) = Fib(i + 1) > n (否则 Fib(i) 不会是最大值斐波那契数小于或等于 n)。

引理 2n - Fib(i) 和 n - Fib(i - 1) .

证明:这些很容易证明。留给读者作为练习。

我最初认为这会导致递归F(n) = F(n - Fib(i)) + F(n - Fib(i - 1)),但是有一个问题:可能是 n - Fib(i - 1) ≥ Fib(i - 1),所以在这种情况下可能会发生 F(i - 1) 被重用,这是我们不允许的。不过,我们可以很容易地解决这个问题:我们可以给函数 F 一个额外的布尔标志,告诉它不允许递归到 F(n - Fib(i)).

最后一个问题仍然存在:如何计算 i?一个重要的观察结果是斐波那契数呈指数增长,因此我们有 i = O(log n)。我们可以使用蛮力来找到它(计算所有斐波那契数,直到 n

function F(n : Integer, recurseHigh = True: Bool):
    if n == 0: return 1
    a, b = 1, 1
    while a + b <= n:
        a, b = b, a + b
    res = 0
    if recurseHigh: res += F(n - b)
    res += F(n - a, n - a < a)
    return res

即使使用这种 32 位整数的“愚蠢”实现,它的运行速度也足够快。如果你使用记忆化,它确实适用于更大的数字,但你需要动态分配内存。

我还没有证明它的运行时复杂性,但是如果使用 memoization,它肯定会非常快。我认为这是 O(log² n) 加法,如果我们将斐波那契数预先计算到 n 将是 O(log n * log log n) > 并对 i 进行二分搜索。虽然不确定没有记忆的情况,但它似乎不适用于 n 超过 2³²。

如果您有兴趣,这里有一些 F 的值,使用 Python 中上述函数的记忆版本计算:

F(0) = 1
F(1) = 2
F(2) = 2
F(3) = 3
F(4) = 3
F(5) = 3
F(6) = 4
F(7) = 3
F(8) = 4
F(9) = 5
F(10) = 4
F(11) = 5
F(12) = 4
F(13) = 4
F(14) = 6
F(4079078553298575003715036404948112232583483826150114126141906775660304738681982981114711241662261246) = 70875138187634460005150866420231716864000000
F(2093397132298013861818922996230028521104292633652443820564201469339117288431349400794759495467500744) = 157806495228764859558469497848542003200000000
F(1832962638825344364456510906608935117588449684478844475703210731222814604429713055795735059447061144) = 9556121706647393773891318116777984000000000
F(6529981124822323555642594388843027053160471595955101601272729237158412478312608142562647329142961542) = 7311968902691913059222356326906593280000000
F(3031139617090050615428607946661983338146903521304736547757088122017649742323928728412275969860093980) = 16200410965370556030391586130218188800000000
F(4787808019310723702107647550328703908551674469886971208491257565640200610624347175457519382346088080) = 7986384770542363809305167745746206720000000
F(568279248853026201557238405432647353484815906567776936304155013089292340520978607228915696160572347) = 213144111166298008572590523452227584000000000
F(7953857553962936439961076971832463917976466235413432258794084414322612186613216541515131230793180511) = 276031486797406622817346654015856836608000000
F(2724019577196318260962320594925520373472226823978772590344943295935004764155341943491062476123088637) = 155006702456719127405700915456167116800000000
F(4922026488474420417379924107498371752969733346340977075329964125801364261449011746275640792914985997) = 3611539307706585535227777776416785118003200
F(10^1000) = 1726698225267318906119107341755992908460082681412498622901372213247990324071889761112794235130300620075124162289430696248595221333809537338231776141120533748424614771724793270540367766223552120024230917898667149630483676495477354911576060816395737762381023625725682073094801703249961941588546705389069111632315001874553269267034143125999981126056382866927910912000000000000000000000000000000000000000000000000000000000000000000000000000000

我们观察到它看起来像 F(n) = Θ(sqrt(n)),这是我尚未证明的另一个结果。

更新:这是 Python 代码:

memo = {}
def F(n, x=True):
    if n == 0: return 1
    if (n, x) in memo: return memo[n,x]
    i = 1
    a, b = 1, 1
    while b + a <= n:
        a, b = b, a + b
    memo[n,x] = (F(n - b) if x else 0) + F(n - a, n - a < a)
    return memo[n,x]

更新 2:通过使用二分搜索查找 i 并使用快速计算 Fib(i),即使没有记忆,您也可以获得更好的运行时间矩阵求幂。不过可能不值得付出努力,尤其是对于 32 位 n。

更新 3:只是为了好玩,这里有一个可证明只做 O(log n) 添加的实现:

fib = [0,1]
def greedy(n):
    while fib[-1] < n:
        fib.append(fib[-1] + fib[-2])
    i = 1
    while fib[i+1] <= n: i += 1
    digs = set()
    while n:
        while fib[i] > n: i -= 1
        digs.add(i)
        n -= fib[i]
    return digs

def F(n):
    digs = greedy(n)
    top = max(digs)
    dp = [[[0,0,0] for _ in xrange(4)] for _ in xrange(top+1)]
    for j in xrange(0, 2): dp[0][j][0] = 1
    for i in xrange(1, top + 1):
        for j in xrange(0,2):
            for k in xrange(0,j+1):
                if i in digs:
                    dp[i][j][k] = dp[i-1][k+j][j] + dp[i-1][k+j+1][j+1]
                else:
                    dp[i][j][k] = dp[i-1][k+j][j] + dp[i-1][k+j-1][j-1]
    return dp[top][0][0]

它首先在斐波那契基数中找到数字的贪心表示,然后使用 DP 找到在该表示中结转数字以构建最终数字的方法数。 dp[i,j,k] 是表示斐波那契基数前缀 1..i 的方式的数量,如果我们将 j 进位到位置 i 并将 k 进位到位置 i - 1。使用它,我们可以在 5 秒内计算出F(10^50000)(结果有超过 20000 个十进制数字!)

【讨论】:

  • F 值的尾随零是可疑的。某处是否正在进行舍入?
  • @Mark:不,Python 有 bigints,所有的计算都是精确的。事实上,2 和 5 的幂相当高也就不足为奇了。素数的分布似乎从低素数到高素数呈指数下降。
  • 是的,我知道 Python 有 bigints。 :-) 如果这些输出数字是正确的(经过一些实验,我相信它们是正确的),结果会非常有趣:当我分解其中任何一个时,我总是得到相对较小的素因数。
  • @Marc:我添加了 Python 源代码,它不使用浮点。是的,无论这意味着什么,我看到质数指数(exp(2) ~ 2*exp(3) ~ 2*exp(5) 等)呈指数下降。
  • @NiklasB。我发现了一个真正了不起的解决方案,这个评论框太小而无法包含。 :-)(向费马道歉。)我已经在答案中发布了解释。
【解决方案2】:

我对@9​​87654321@ 的答案的两个方面很感兴趣:计算速度(即使对于巨大的数字),以及结果倾向于具有小的素因数。这些暗示可以将解决方案计算为小项的乘积,事实证明确实如此。

为了解释发生了什么,我需要一些符号和术语。对于任何非负整数n,我将n 的(唯一)贪婪表示定义为通过重复取不超过n 的最大斐波那契数获得的斐波那契数之和。例如,10 的贪心表示是8 + 2。很容易观察到,我们从未在如此贪婪的表示中使用Fib(1)

我还想要一种紧凑的方式来编写这些表示,为此我将使用位串。很像二进制,除了位值遵循斐波那契数列而不是 2 的幂数列,我会先写最低有效位。例如00100001 在位置2 和位置7 中有1s,所以代表Fib(2) + Fib(7) = 1 + 13 = 14。 (是的,我从0 开始计数,并遵循Fib(0) = 0 的惯例。)

找到所有表示的蛮力方法是从贪婪表示开始,然后探索将001 形式的子模式重写为110 形式的模式的所有可能性;即,对于某些 k,将 Fib(k+2) 替换为 Fib(k) + Fib(k+1)

所以我们总是可以将n 的贪心表示写为位串,并且该位串将是0s 和1s 的序列,没有两个相邻的1s。现在关键的观察是我们可以将这个比特串分成几部分,并计算每个单独部分的重写次数,乘以得到表示的总数。这是有效的,因为位串中的某些子模式阻止了模式左侧字符串部分的重写规则与右侧的重写规则之间的交互。

例如,让我们看一下n = 78。它的贪婪表示是00010000101,并且一种蛮力方法可以快速识别全套表示。其中有十个:

00010000101
01100000101
00010011001
01100011001
00011101001
01101101001
0001001111
0110001111
0001110111
0110110111

我们可以将模式的第一部分 0001 与第二部分 0000101 分开。上面的每个组合都来自重写0001,分别重写0000101,并将两个重写粘合在一起。模式的左侧部分有 2 次重写(包括原版),右侧有 5 次重写,因此我们总共得到 10 种表示形式。

使这项工作有效的原因是左半部分0001 的任何重写都以0110 结尾,而右半部分的任何重写都以0011 开头。因此,与边界重叠的 001110 都不匹配。只要我们有两个由 偶数 个零分隔的 1s,我们就会得到这种分隔。

这解释了 Niklas 的回答中看到的小素因数:在随机选择的数字中,会有许多偶数长度的 0s 序列,每个序列都代表一个我们可以拆分计算的点。

解释变得有点复杂,所以这里有一些 Python 代码。我已经验证结果与 Niklas 的所有n 直到10**6 以及随机选择的大n 的选择一致。它应该具有相同的算法复杂度。

def combinations(n):
    # Find Fibonacci numbers not exceeding n, along with their indices.
    # We don't need Fib(0) or Fib(1), so start at Fib(2).
    fibs = []
    a, b, index = 1, 2, 2
    while a <= n:
        fibs.append((index, a))
        a, b, index = b, a + b, index + 1

    # Compute greedy representation of n as a sum of Fibonacci numbers;
    # accumulate the indices of those numbers in indices.
    indices = []
    for index, fib in reversed(fibs):
        if n >= fib:
            n -= fib
            indices.append(index)
    indices = indices[::-1]

    # Compute the 'signature' of the number: the lengths of the pieces
    # of the form 00...01.
    signature = [i2 - i1 for i1, i2 in zip([-1] + indices[:-1], indices)]

    # Iterate to simultaneously compute total number of rewrites,
    # and the total number with the top bit set.
    total, top_set = 1, 1
    for l in signature:
        total, top_set = ((l + 2) // 2 * total - (l + 1) % 2 * top_set, total)

    # And return the grand total.
    return total

编辑:大大简化了代码。


编辑2:我刚刚再次遇到这个答案,并怀疑有更直接的方法。这是另一个代码简化,清楚地表明需要O(log n) 操作。

def combinations(n):
    """Number of ways to write n as a sum of positive
    Fibonacci numbers with distinct indices.
    """
    # Find Fibonacci numbers not exceeding n.
    fibs = []
    fib, next_fib = 0, 1
    while fib <= n:
        fibs.append(fib)
        fib, next_fib = next_fib, fib + next_fib

    # Compute greedy representation, most significant bit first.
    greedy = []
    for fib in reversed(fibs):
        greedy.append(fib <= n)
        if greedy[-1]:
            n -= fib

    # Iterate to compute number of rewrites.
    x, y, z = 1, 0, 0
    for bit in reversed(greedy):
        x, y, z = (0, y + z, x) if bit else (x + z, z, y)
    return y + z

【讨论】:

  • 很好的观察结果。事实上,如果您使用斐波那契数作为数字系统的基础,则会有许多有趣的属性,例如作为不同斐波那契数之和的表示也存在,并且如果您要求所选斐波那契数的索引是唯一的至少相隔 2(这基本上是我的回答所依据的)。
  • 嗯;实际上,代码可以大大简化,尽管这掩盖了结果是产品的事实。我会更新的。
  • @NiklasB。是的;这就是我包含(unique) 提示的原因。我准备在答案中包含多少信息是有限度的。 (我曾经教过这些东西,但我从来没有遇到过有多少表示的问题。)
  • 还有另一种通用的方法来计算重新排列表示的方法的数量,它适用于各种数字系统,例如balanced ternary、四进制等带有负数(这样表示不是唯一的):设f(i,j,k) 是表示特定表示的前缀1..i 的方法数(例如贪婪的表示),其中j 是位置i 的进位,k 是进位到i - 1的位置。
  • 显然f(i,j,k) = 0 if max(j,k) &gt; i,所以我们有有限数量的非零状态。如果贪心表示的第 i 个数字是1,我们有f(i,j,k) = f(i-1, k+j, j) + f(i-1, k+j+1, j+1),否则为f(i,j,k) = f(i-1, k+j-1, k+j-1) + f(i-1, k+j, j)。在这里很容易看到 O(log^2 n) 运行时,我的代码避免显式状态转换的方式有点奇怪,我不太清楚它为什么会起作用
【解决方案3】:

您可以通过取小于或等于您的数字的最大斐波那契数,减去该数,然后取小于或等于余数的下一个最大的斐波那契数等,在 Fibonacci-base 中找到字典顺序上最大的 0/1 表示。那么问题是如何从字典上最大的一个中找到斐波那契基中的所有其他 0/1 表示。我相信你可以使用斐波那契递归关系来做到这一点。例如,如果您的表示是 1100...,那么您可以将表示中的第二大 Fib 数替换为接下来的两个之和,得到 1011.....如果您以这种方式从左到右反复递归处理字符串,无论何时选择是否进行替换,并使用动态编程来记住您已经探索过哪些表示,我相信您将获得所有表示,并且在 O(m log n) 时间内,其中 m 是总数您的数字 n 的斐波那契表示。如果我找到确切的证据,我会更新。同时,您可以检查猜想的数字高达约一百万左右。如果它检查了所有这些情况,那么总体上几乎可以肯定是正确的。

【讨论】:

  • 由于不能保证“下一个”最大的数字会成为有效解决方案的一部分,我相信这种方法仍然是指数的。
【解决方案4】:

python 中一种天真的可能性(在合理的时间内工作到 10^6)

def nfibhelper(fibm1,fibm2,n):
    fib = fibm1 + fibm2
    if fib > n:
        return 0
    r=0
    if n == fib :
        r+=1
    return r + nfibhelper(fibm2,fib,n-fib) + nfibhelper(fibm2,fib,n)


def F(n):
    return nfibhelper(1,0,n) ##1 will be used twice as fib

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-01-18
    • 1970-01-01
    • 1970-01-01
    • 2021-12-21
    • 2020-04-02
    • 1970-01-01
    • 2017-05-08
    • 1970-01-01
    相关资源
    最近更新 更多