Вычислить число представлений числа как сумму чисел Фибоначчи
Моя группа изо всех сил пыталась найти хороший алгоритм, но все, что мы могли придумать, было экспоненциальным. Есть ли способ сделать это быстрее? Вот полный вопрос:
определить функцию
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, но это не помогло бы, так как динамические массивы все равно не были разрешены. Итак, как мы можем избежать экспоненциальной сложности времени?
4 ответов
давайте докажем некоторые вещи о числах:
Лемма 1: да n ≥ 1 целое число, а Fib (i) быть наибольшее число Фибоначчи с Fib (i) ≤ n. Тогда в представлении n как сумма чисел Фибоначчи с различными индексами, либо Fib (i) или Fib (i - 1) появляется, но не оба.
доказательство: мы можем показать по индукции, что sum Fib(1) + Fib(2) + ... + Fib(i - 2) = Fib (i) - 1. С Fib (i) , нам нужно хотя бы Fib (i - 1) или Fib (i) в представительстве. Но не оба, так как приврал(я) + Фиб(я - 1) = приврал(я + 1) > N (в противном случае Fib (i) не будет максимальным числом Фибоначчи меньше или равно n).
Лемма 2: N-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 = 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, он работает даже для гораздо больших чисел, но тогда вам нужна динамически выделенная память.
я еще не доказал сложность выполнения этого, но это чертовски быстро, если используется memoization. Я думаю, это O (log2 n) дополнения и будет O (log n * log log n) если мы предварительно вычисляем числа Фибоначчи до n и бинарным поиск я. Не уверен в случае без memoization, хотя, похоже, он не работает хорошо с n за 232.
вот некоторые значения F если вам интересно, вычисляется с memoized версией вышеуказанной функции в 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: вы можете получить лучшее время выполнения даже без memoization с помощью двоичного поиска, чтобы найти я и вычислений 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
числа в базе Фибоначчи, если у нас есть carry j
на должность i
и снести k
на должность i - 1
. Используя это, мы можем вычислить F(10^50000)
менее чем за 5 секунд (результат имеет более 20000 десятичных цифр!)
меня заинтриговали два аспекта Никлас Б.ответ: скорость вычисления (даже для огромных чисел) и тенденция результатов иметь малые простые коэффициенты. Они намекают, что решение может быть вычислено как произведение малых членов, и это действительно так.
чтобы объяснить, что происходит, мне нужны некоторые обозначения и терминология. Для любого неотрицательного целого n
, я определю (уникальный)жадина представление of n
быть суммой чисел Фибоначчи, полученных многократно принимая наибольшее число Фибоначчи, не превышающее n
. Так, например, жадное представление 10
is 8 + 2
. Легко заметить, что мы никогда не используем Fib(1)
в таком жадном представлении.
я также хочу компактный способ написать эти представления, и для этого я собираюсь использовать bitstrings. Очень похоже на binary, за исключением того, что значения места следуют за Фибоначчи последовательность вместо последовательности степеней 2, и я напишу с наименее значимой цифрой первым. Так, например,00100001
и 1
в должности 2
и позицию 7
, таким образом, представляет Fib(2) + Fib(7) = 1 + 13 = 14
. (Да, я начинаю считать на 0
, и следуя обычному соглашению, что Fib(0) = 0
.)
способ грубой силы найти все представления-начать с жадного представления, а затем исследовать все возможности для перезаписи подшаблона формы 001
в шаблон вида 110
; т. е., заменив Fib(k+2)
С Fib(k) + Fib(k+1)
для некоторых k
.
поэтому мы всегда можем написать жадное представление n
как битовая строка, и эта битовая строка будет последовательностью 0
s и 1
s, без двух смежных 1
s. Теперь ключевое наблюдение заключается в том, что мы можем разбить эту битовую строку на части и вычислить количество перезаписей для каждой отдельной части, умножая, чтобы получить общее количество представлений. Это работает, потому что некоторые подзаголовки в bitstring предотвращают взаимодействие между правилами перезаписи для части строки слева от шаблона и тех, кто справа.
для примера, давайте посмотрим на n = 78
. Его жадное представление -00010000101
, и подход грубой силы быстро идентифицирует полный набор представлений. Их десять:
00010000101
01100000101
00010011001
01100011001
00011101001
01101101001
0001001111
0110001111
0001110111
0110110111
мы можем отделить первую часть рисунка,0001
из второго, 0000101
. Каждая из приведенных выше комбинаций происходит от переписывания 0001
, отдельно рерайтинг 0000101
, и склеивание двух перезаписей вместе. Есть 2 перезаписи (включая оригинал) для левой части шаблона и 5 для правой, поэтому мы получаем 10 представлений в целом.
что делает эту работу, так это то, что любая перепись левой половины,0001
, заканчивается либо 01
или 10
, в то время как любая перепись правой половины начинается с 00
или 11
. Так нет ни 001
или 110
это перекрывает границу. Мы получим это разделение, когда у нас будет два 1
s разделены даже количество нулей.
и это объясняет небольшие простые факторы, замеченные в ответе Никласа: в случайно выбранном числе будет много последовательностей 0
s четной длины, и каждый из них представляет собой точку, где мы можем разделить вычисление.
объяснения становятся немного запутанный, так что вот код Python. Я проверил, что результаты согласуются с результатами Никласа для всех 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
EDIT: код существенно упрощен.
EDIT 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
вы можете найти лексикографически наибольшее представление 0/1 в базе Фибоначчи, взяв наибольшее число Фибоначчи меньше или равно вашему числу, вычесть это, а затем взять следующее по величине число Фибоначчи меньше или равно остатку и т. д. Тогда возникает вопрос, как найти все остальные 0/1 представления в базе Фибоначчи из лексикографически наибольшего. Я считаю, что вы можете использовать рекуррентное отношение Фибоначчи для этого. Например, если ваше представление 1100... затем вы можете заменить 2-е по величине число Fib в представлении суммой следующих двух, давая 1011.....Если вы рекурсивно обрабатываете строку таким образом слева направо несколько раз, либо выбирая замену, либо нет, когда вы можете, и используя динамическое программирование, чтобы помнить, какие представления вы уже исследовали, я считаю, что вы получите все представления, и в O(M log n) время, где m-общее количество представлений Фибоначчи для вашего номер n. Я сообщу, если найду убедительные доказательства. Тем временем вы можете проверить гипотезу на число до миллиона или около того. Если он проверяет, на все эти случаи, то это почти наверняка так.
одна из наивных возможностей в 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