Глава 9. Числа Фибоначчи
Требуется написать программу, вычисляющую и выводящую на экран число Фибоначчи с заданным номером в последовательности. Номер передаётся в программу через командную строку:
%./fibonacci.pl 68%./fibonacci.pl 1189
| Готовая программа | Идеи реализации |
Как вычислить число фибоначчи
Последовательность чисел Фибоначчи определяется формулой Fn = Fn-1 + Fn-2 . То есть, следующее число получается как сумма двух предыдущих.
Первые два числа равны 1 , затем 2(1+1) , затем 3(1+2) , 5(2+3) и так далее: 1, 1, 2, 3, 5, 8, 13, 21. .
Числа Фибоначчи тесно связаны с золотым сечением и множеством природных явлений вокруг нас.
Напишите функцию fib(n) которая возвращает n-е число Фибоначчи.
function fib(n) < /* ваш код */ >alert(fib(3)); // 2 alert(fib(7)); // 13 alert(fib(77)); // 5527939700884757
P.S. Все запуски функций из примера выше должны работать быстро. Вызов fib(77) должен занимать не более доли секунды.
Сначала решим через рекурсию.
Числа Фибоначчи рекурсивны по определению:
function fib(n) < return n alert( fib(3) ); // 2 alert( fib(7) ); // 13 // fib(77); // вычисляется очень долго
При больших значениях n такое решение будет работать очень долго. Например, fib(77) может повесить браузер на некоторое время, съев все ресурсы процессора.
Это потому, что функция порождает обширное дерево вложенных вызовов. При этом ряд значений вычисляется много раз снова и снова.
Например, посмотрим на отрывок вычислений для fib(5) :
. fib(5) = fib(4) + fib(3) fib(4) = fib(3) + fib(2) .
Здесь видно, что значение fib(3) нужно одновременно и для fib(5) и для fib(4) . В коде оно будет вычислено два раза, совершенно независимо.
Полное дерево рекурсии:
Можно заметить, что fib(3) вычисляется дважды, а fib(2) – трижды. Общее количество вычислений растёт намного быстрее, чем n , что делает его огромным даже для n=77 .
Можно это оптимизировать, запоминая уже вычисленные значения: если значение, скажем, fib(3) вычислено однажды, затем мы просто переиспользуем это значение для последующих вычислений.
Другим вариантом было бы отказаться от рекурсии и использовать совершенно другой алгоритм на основе цикла.
Вместо того, чтобы начинать с n и вычислять необходимые предыдущие значения, можно написать цикл, который начнёт с 1 и 2 , затем из них получит fib(3) как их сумму, затем fib(4) как сумму предыдущих значений, затем fib(5) и так далее, до финального результата. На каждом шаге нам нужно помнить только значения двух предыдущих чисел последовательности.
Вот детальные шаги нового алгоритма.
// a = fib(1), b = fib(2), эти значения по определению равны 1 let a = 1, b = 1; // получим c = fib(3) как их сумму let c = a + b; /* теперь у нас есть fib(1), fib(2), fib(3) a b c 1, 1, 2 */
Теперь мы хотим получить fib(4) = fib(2) + fib(3) .
Переставим переменные: a,b , присвоим значения fib(2),fib(3) , тогда c можно получить как их сумму:
a = b; // теперь a = fib(2) b = c; // теперь b = fib(3) c = a + b; // c = fib(4) /* имеем последовательность: a b c 1, 1, 2, 3 */
Следующий шаг даёт новое число последовательности:
a = b; // now a = fib(3) b = c; // now b = fib(4) c = a + b; // c = fib(5) /* последовательность теперь (на одно число больше): a b c 1, 1, 2, 3, 5 */
…И так далее, пока не получим искомое значение. Это намного быстрее рекурсии и не требует повторных вычислений.
function fib(n) < let a = 1; let b = 1; for (let i = 3; i return b; > alert( fib(3) ); // 2 alert( fib(7) ); // 13 alert( fib(77) ); // 5527939700884757
Цикл начинается с i=3 , потому что первое и второе значения последовательности заданы a=1 , b=1 .
Числа Фибоначчи: циклом и рекурсией
Числа Фибоначчи – это ряд чисел, в котором каждое следующее число равно сумме двух предыдущих.
Иногда ряд начинают с нуля.
В данном случае мы будем придерживаться первого варианта.
Вычисление n-го числа ряда Фибоначчи с помощью цикла while
Присвоим переменным fib1 и fib2 значения двух первых элементов ряда, то есть единицы.
Получим от пользователя номер элемента, значение которого требуется вычислить. Присвоим номер элемента переменной n .
Поскольку значения первых двух элементов ряда Фибоначчи нам уже известны и вычисления начинаем с третьего, количество проходов по телу цикла должно быть на 2 меньше значения n , то есть n - 2 .
Если пользователь вводит 1 или 2, тело цикла ни разу не выполняется, на экран выводится исходное значение fib2 .
- Сложить fib1 и fib2 , присвоив результат переменной для временного хранения данных, например, fib_sum .
- Переменной fib1 присвоить значение fib2 .
- Переменной fib2 присвоить значение fib_sum .
После окончания работы цикла вывести значение fib2 на экран.
fib1 = 1 fib2 = 1 n = input("Номер элемента ряда Фибоначчи: ") n = int(n) i = 0 while i n - 2: fib_sum = fib1 + fib2 fib1 = fib2 fib2 = fib_sum i = i + 1 print("Значение этого элемента:", fib2)
Пример выполнения программы:
Номер элемента ряда Фибоначчи: 10 Значение этого элемента: 55
Компактный вариант кода:
fib1 = fib2 = 1 n = input("Номер элемента ряда Фибоначчи: ") n = int(n) - 2 while n > 0: fib1, fib2 = fib2, fib1 + fib2 n -= 1 print("Значение этого элемента:", fib2)
Вывод ряда чисел Фибоначчи с помощью цикла for
В данном случае выводится не только значение искомого элемента ряда Фибоначчи, но и все числа до него включительно. Для этого вывод значения fib2 помещен в цикл.
fib1 = fib2 = 1 n = int(input()) print(fib1, fib2, end=' ') for i in range(2, n): fib1, fib2 = fib2, fib1 + fib2 print(fib2, end=' ')
10 1 1 2 3 5 8 13 21 34 55
Рекурсивное вычисление n-го числа ряда Фибоначчи
- Если n = 1 или n = 2, вернуть в вызывающую ветку единицу, так как первый и второй элементы ряда Фибоначчи равны единице.
- Во всех остальных случаях вызвать эту же функцию с аргументами n - 1 и n - 2. Результат двух вызовов сложить и вернуть в вызывающую ветку программы.
def fibonacci(n): if n in (1, 2): return 1 return fibonacci(n - 1) + fibonacci(n - 2) print(fibonacci(10))
Допустим, n = 4. Тогда произойдет рекурсивный вызов fibonacci(3) и fibonacci(2). Второй вернет единицу, а первый приведет к еще двум вызовам функции: fibonacci(2) и fibonacci(1). Оба вызова вернут единицу, в сумме будет два. Таким образом, вызов fibonacci(3) возвращает число 2, которое суммируется с числом 1 от вызова fibonacci(2). Результат 3 возвращается в основную ветку программы. Четвертый элемент ряда Фибоначчи равен трем: 1 1 2 3.
X Скрыть Наверх
Решение задач на Python
Находим N'е число Фибоначчи тремя способами за приемлемое время: основы динамического программирования
Очень часто на разнообразных олимпиадах попадаются задачи вроде этой, которые, как думается на первый взгляд, можно решить с помощью простого перебора. Но если мы подсчитаем количество возможных вариантов, то сразу убедимся в неэффективности такого подхода: например, простая рекурсивная функция, приведенная ниже, будет потреблять существенные ресурсы уже на 30-ом числе Фибоначчи, тогда как на олимпиадах время решения часто ограничено 1-5 секундами.
int fibo(int n) < if (n == 1 || n == 2) < return 1; >else < return fibo(n - 1) + fibo(n - 2); >>
Давайте подумаем, почему так происходит. Например, для вычисления fibo(30) мы сначала вычисляем fibo(29) и fibo(28). Но при этом наша программа «забывает», что fibo(28) мы уже вычисляли при поиске fibo(29).
Основная ошибка такого подхода «в лоб» в том, что одинаковые значения аргументов функции исчисляются многократно — а ведь это достаточно ресурсоемкие операции. Избавиться от повторяющихся вычислений нам поможет метод динамического программирования — это прием, при использовании которого задача разбивается на общие и повторяющиеся подзадачи, каждая из которых решается только 1 раз — это значительно повышает эффективность программы. Этот метод подробно описан в нашей статье, там же есть и примеры решения других задач.
Самый просто вариант улучшения нашей функции — запоминать, какие значения мы уже вычисляли. Для этого нужно ввести дополнительный массив, который будет служить как бы «кэшем» для наших вычислений: перед вычислением нового значения мы будем проверять, не вычисляли ли его раньше. Если вычисляли, то будем брать из массива готовое значение, а если не вычисляли — придётся считать его на основе предыдущих и запоминать на будущее:
int cache[100]; int fibo(int n) < if (cache[n] == 0) < if (n == 1 || n == 2) < cache[n] = 1; >else < cache[n] = fibo(n - 1) + fibo(n - 2); >> return cache[n]; >
Так как в данной задаче для вычисления N-ого значения нам гарантированно понадобится (N-1)-е, то не составит труда переписать формулу в итерационный вид — просто будем заполнять наш массив подряд до тех пор, пока не дойдём до нужной ячейки:
cache[0] = 1; cache[1] = 1; for (int i = 2; i cout
Теперь мы можем заметить, что когда мы вычисляем значение F(N), то значение F(N-3) нам уже гарантированно никогда не понадобится. То есть нам достаточно хранить в памяти лишь два значения — F(N-1) и F(N-2). Причём, как только мы вычислили F(N), хранение F(N-2) теряет всякий смысл. Попробуем записать эти размышления в виде кода:
//Два предыдущих значения: int cache1 = 1; int cache2 = 1; //Новое значение int cache3; for (int i = 2; i через итерацию потеряет актуальность cache2, т.е. он и должен стать cache1 //Иными словами, cache1 -- f(n-2), cache2 -- f(n-1), cache3 -- f(n). //Пусть N=n+1 (номер, который мы вычисляем на следующей итерации). Тогда n-2=N-3, n-1=N-2, n=N-1. //В соответствии с новыми реалиями мы и переписываем значения наших переменных: cache1 = cache2; cache2 = cache3; > cout
Бывалому программисту понятно, что код выше, в общем-то ерунда, так как cache3 никогда не используется (он сразу записывается в cache2 ), и всю итерацию можно переписать, используя всего одно выражение:
cache[0] = 1; cache[1] = 1; for (int i = 2; i cout
Для тех, кто не может понять, как работает магия с остатком от деления, или просто хочет увидеть более неочевидную формулу, существует ещё одно решение:
int x = 1; int y = 1; for (int i = 2; i < n; i++) < y = x + y; x = y - x; >cout
Попробуйте проследить за выполнением этой программы: вы убедитесь в правильности алгоритма.
На данный момент этот блок не поддерживается, но мы не забыли о нём! Наша команда уже занята его разработкой, он будет доступен в ближайшее время.
P.S. Вообще, существует единая формула для вычисления любого числа Фибоначчи, которая не требует никаких итераций или рекурсии:
const double SQRT5 = sqrt(5); const double PHI = (SQRT5 + 1) / 2; int fibo(int n)< return int(pow(PHI, n) / SQRT5 + 0.5); >
Но, как можете догадаться, подвох в том, что цена вычисления степеней нецелых чисел довольно велика, как и их погрешность.
