.NET

индекс
121,03

Числа Фибоначчи (этюд на C#)

Наверное многим студентам приходилось изучать рекурсию на примере вычисления чисел Фибоначчи. Задачка это безусловно академическая, и рекурсию она иллюстрирует явно хуже чем вычисление, скажем, факториалов, но она интересна тем, что имеет много решений разной степени извращенности. В этом посте – небольшой этюд на эту тему.


Думаю что никого не удивит “дефолтное” решение для вычисления N-го числа Фибоначчи:

static int fib(int n)
{
  return n > 1 ? fib(n - 1) + fib(n - 2) : n;
}

Также, есть достаточно “модная” форма записи этого решения которая использует Y-комбинатор:

Func<intint> fib  = Y<int,int>(f => n => n > 1 ? f(n - 1) + f(n - 2) : n);

Где Y определен, к примеру, вот так:

static Func<A, R> Y<A, R>(Func<Func<A, R>, Func<A, R>> f)
{
  Func<A, R> g = null;
  g = f(a=>g(a));
  return g;
}

Для больших N, такой подход непродуктивен. Как посчитать число N быстрее? Для начала давайте вспомним, почему например x*x считается быстрее чем Math.Pow(x,2)? Потому что для целочисленной степени можно не только обойтись без рядов Тейлора, но также оптимизировать вычисление для больших степеней путем создания временных переменных. Например, x4 можно считать как int y = x * x; return y * y; – и чем больше степень, тем больше экономия.

К чему это я? К тому что число Фибоначчи можно рассчитать с помощью следующей формулы:




Теперь понятно зачем нам целочисленное возведение в степень? Для матриц можно делать то же самое, только сначала нужно понять как это вообще делается. На просторах интеренета я нашел возможно идеальный алгоритм, который оптимизирует целочисленное возведение в степень. Прошу заметить, что в примере ниже степень имеет тип short.

public static long IntPower(int x, short power)
{
  if (power == 0) return 1;
  if (power == 1) return x;
  int n = 15;
  while ((power <<= 1) >= 0) n--;
  long tmp = x;
  while (--n > 0)
    tmp = tmp * tmp *
         (((power <<= 1) < 0) ? x : 1);
  return tmp;
}

Теперь осталось только определить матрицу 2×2. Тут можно было бы воспользоваться какой-то библиотечкой, но я решил написать самую простую возможную структуру:

struct mtx2x2
{
  public int _11, _12, _21, _22;
  public static mtx2x2 operator*(mtx2x2 lhs, mtx2x2 rhs)
  {
    return new mtx2x2
             {
               _11 = lhs._11*rhs._11 + lhs._12*rhs._21,
               _12 = lhs._11*rhs._12 + lhs._12*rhs._22,
               _21 = lhs._21*rhs._11 + lhs._22*rhs._21,
               _22 = lhs._21*rhs._12 + lhs._22*rhs._22
             };
  }
}

После этого нужно было определить две константы которые нам пригодятся – ту матрицу которую мы будет возводить в степень и единичную матрицу:

private static readonly mtx2x2 fibMtx = new mtx2x2 {_11 = 1, _12 = 1, _21 = 1};
private static readonly mtx2x2 identity = new mtx2x2 {_11 = 1, _22 = 1};

Теперь мы можем переписать метод IntPower() для матриц 2×2:

public static mtx2x2 IntPower(mtx2x2 x, short power)
{
  if (power == 0) return identity;
  if (power == 1) return x;
  int n = 15;
  while ((power <<= 1) >= 0) n--;
  mtx2x2 tmp = x;
  while (--n > 0)
    tmp = (tmp * tmp) * (((power <<= 1) < 0) ? x : identity);
  return tmp;
}

И определить новый метод для вычисления числа Фибоначчи:

static int fibm(short n)
{
  return IntPower(fibMtx, (short)(n-1))._11;
}

Вот и все. Думаю что сравнивать производительность бессмысленно – у меня fib(40) считается 4 секунды, а fibm(40) выводится моментально. ■
+22
6 февраля 2010, 14:03
31

комментарии (52)

+9
mrded #
+2
imploid #
Мне кажется что C# не оптимизирует хвостовую рекурсию (хотя я могу ошибаться).
Интереснее было бы для сравнения привести вариант на F# (и сравнить скорость).
+1
Dimchansky #
На сволько мне известно, на уровне MSIL хвостовая рекурсия поддерживается, но сейчас C# компилятор это не использует и ситуацию вроде как обещали исправить с выходом .NET 4.
0
0re1 #
Забыли еще про способ с возведением в степень золотого сечения.
Он менее эффективен(целые перемножаются быстрее) и дает неточный ответ, но вспомнить про него надо было.
+5
qrazydraqon #
Вспоминать про него необязательно, так как на самоме деле описанный способ можно слегка переделать и получить то, что Вы говорите, а именно: если расписать матрицу [[1,1],[1,0]] в жордановой форме, то там получится диагональная матрица, у которой на месте (1,1) как раз золотое сечение. Ну и степень будет соответственная.
Но, как правильно отмечено, перемножать и складывать единички получается быстрее, чем иррациональное число в степень возводить.
+1
qrazydraqon #
Или на месте (2,2), это смотря как жорданку расписывать.
+3
mace #
[занудство]
Все же для чисел фибоначчи с номером в районе 40 быстрее и понятнее будет так:
    static int fib(int n)
    {
      int[] f = new int[3];
      f[1] = 1;
      for (int i = 2; i <= n; i++)
      {
        f[i%3] = f[(i + 1)%3] + f[(i + 2)%3];
      }
      return f[n%3];
    }


* This source code was highlighted with Source Code Highlighter.

А дальше все равно нужно либо считать по модулю, либо реализовывать длинную арифметику (как с ней обстоят дела в фреймворке 4.0, кстати?). ДА проще реализовать для сочетания двух длинных чисел, чем для умножения. В общем за матрицу вам плюс, а вот пример, зачем она нужна, неудачен: матрица даст существенный прирост в производительности только при вычислении чисел фибоначчи по модулю с номером в районе >10^8, а если с ДА, то >1000 (чисто интуитивно, возможно я и ошибаюсь).
[/занудство]
0
0re1 #
Вы неправы.
Ваш алгоритм работает за O(n)
А описаный в статье за O(lg(n))
0
0re1 #
Даже с учетом медленного умножения при n > 100 алгоритм из статьи будет быстрее.
0
mace #
специально написал простой код для замера производительности.
он становится ощутимо быстрее (в 2 раза) при n > 70 с использованием обычных int'ов.
если использовать long'и — при n == 120 они равны.
сейчас попробую померять время с длинной арифметикой в фреймворке 4.0
0
mace #
да, согласен. с длинной арифметикой их .net 4.0 они равны при n == 100
–1
mezastel #
Если сравнивать с рекурсивным применением, то матрица дает прирост примерно при N>10. Конечно реализация с кэшированием намного быстрее на начальном этапе, и мы раньге получим overflow чем матрица начнет обгонять эту реализацию по производительности.

В .Net 4 должен появиться System.Numerics.BigInteger, а вот с его производительностью нужно разбираться.
+1
0re1 #
[занудство]

static int fib(int n)
  {
   int[] f = new int[2];
   f[0] = f[1] = 1;
   for (int i = 2; i <= n; i++)
    f[i & 1] += f[!(i & 1)];
   return f[!(n & 1)];
  }


* This source code was highlighted with Source Code Highlighter.


Так ваш код будет быстрее(без деления по модулю)
[/занудство]
0
mace #
вот только он не компилируется в C# =)
+1
0re1 #
не ожидал от С# такого.
0
mace #
там запрещено использовать операцию "!" для целочисельных переменных.
0
0re1 #
ну тогда заменяем !(i & 1) на (i ^ 1 & 1)
–2
glider #
Сложность алгоритма от этого не изменится, а пытаться руками оптимизировать код на высокоуровневом языке таким образом — дело неблагодарное. Все равно деление с остатком уже давно выполняется на x86 за один такт.
+1
udpn #
Пруфлинк в студию.
0
glider #
Да, про один такт я действительно напарил, чистый CPI куда больше. Что, впрочем, не отменяет основной моей идеи: с подобной оптимизацией транслятор справится куда лучше.
–5
kovpas #
static int fib(int n)
{
  return n > 1 ? fib(n - 1) + fib(n - 2) : n;
}


Садись, два :)
–3
kovpas #
Тьфу, блин, получилось как будто обращаюсь к автору.

Короче, убивал бы за подобные решения «в лоб» :).
+2
mezastel #
Так преподы не настолько продвинуты — они вам за такое как раз 5 поставят, а если начнете «мудрить» они обидятся что де непонятно и поставят пару за то что «умничаете».
+2
fvlad #
Это плохие преподы.
0
mezastel #
Ну, у меня были в основном именно такие…
0
udpn #
Ну вот, опять началось.
зы Хотя уж кто бы говорил.
+3
Flcn #

a, b = b, a+1

по моему это проще )
0
Flcn #
ёпт… a, b = b, a+b
+2
Kind #
Хм, не вижу, честно говоря, интереса играться с рекурсией, как приёмом в программировании, с числами Фибоначчи, для которых, вообще говоря, существует формула Бине для вычисления чисел Фибоначчи без рекурсии.
+1
qrazydraqon #
Когда научитесь быстро возводить в степень иррациональные числа, обращайтесь.
+1
lany #
Показатель-то целый — тот же самый алгоритм работает. Формула Бине может дать более быстрый результат, чем решение в статье.

Впрочем, если мне нужны быстро и точно числа Фибоначчи, я лучше lookup-table заведу. Статья-то как бы не об этом :-)
0
jawbreaker #
>>Формула Бине может дать более быстрый результат
Быстрый, но не точный. Как числа с плавающей точкой в компутере хранятся знаете?
–2
lany #
А вы формулу-то поглядели или просто так пишете?
sub fibo_simple($) {
	my @list = (1,1);
	push @list, $list[-1]+$list[-2] for(3..$_[0]);
	@list;
}
use constant { SQRT5 => sqrt(5), PHI => (sqrt(5)+1)/2 };
sub fibo_binet($) {
	map {int(PHI**$_/SQRT5+.5)} 1..$_[0];
}
use Test::More tests => 1;
is_deeply([fibo_simple(60)],[fibo_binet(60)],
          "Первые 60 чисел — по определению и по формуле Бине");
D:\>perl fibo_test.pl
1..1
ok 1 - Первые 60 чисел — по определению и по формуле Бине
Кстати, 60-е число сильно за пределами 2^32. Извините за перл, но вы легко проведёте тест на своём любимом языке.
0
jawbreaker #
Поставьте вместо 60 70 и увидите про что я говорил
0
jawbreaker #
Блин, 71 вместо 60.
0
qrazydraqon #
О, вот оно. Точно вы большую степень золотого сечения не посчитаете, символьно это вообще повеситься можно. А тут раз-раз, символьно нолики и единички, круто.
0
udpn #
Ну можно организовать числа с плавающей точкой высокой точности или гибрид double/long, где double хранит только дробную часть числа (не знаю, кто придумал, но увидел у П.Митричева) для не очень больших значений N.
Только игра не стоит свеч, лучше всё-таки целочисленно.
+1
lany #
public static mtx2x2 IntPower(mtx2x2 x, short power)
{
  if (power < 2) return x;

Ещё занудство: любая матрица в нулевой степени даст единичную матрицу. У вас не так =)
–11
danilissimus #
>C#
дальше не читал, ага.
+5
mace #
это же надо, не читали дальше окончания заголовка, а все же не поленились зайти внутрь топика и оставить комментарий, чтобы сообщить об этом факте всему миру. ваше упорство достойно лучшего применения ;)
+2
mezastel #
Ну, на Хабре часто статьи связанные с Microsoft минусуют за 5 секунд после публикации. Тут просто человек откровеннее среднего :)
+1
braindamaged #
Иногда кажется, что даже не через 5 после, а за 5 секунд _до_.
+1
KAndy #
Формула Бине — не? ;)
F_n = \frac{\left(\frac{1 + \sqrt{5}}{2}\right)^n - \left(\frac{1 - \sqrt{5}}{2}\right)^n}{\sqrt{5}} = \frac{\phi^n - (-\phi )^{-n}}{\phi - (-\phi )^{-1}}
+1
KAndy #
упс, нужно чаще обновлять страницу :(
–1
bolk #
Факториалы, числа Фибоначчи, я красотой рекурсии проникся, когда увидел рекурсивную сборку ханойской башни. Вот тогда я и захотел понять что это. Алгоритм, кажется, был в «Науке и жизнь» за какой-то бородатый год.
+1
AAbrosov #
для каждого умножения матриц потребуется 8 умножений.
Лучше использовать эти формулы:
F(2n)=F(n+1)^2-F(n-1)^2
F(2n+1)=F(n+1)^2+F(n)^2
0
udpn #
Вот это вы очень кстати сказали. Ведь у матрицы a12=a21 походу, а лишние вычисления ни к чему.

Реквестирую добавление формул в статью.
+2
UshkurKayuf #
Что-то мне странно, человек пишет об «идеальном алгоритме», а в пример даже вариант с мемоизацией не привел.

Например, такой:
Func<ulong, ulong> fib = null;
fib = (x) => x > 1? fib(x-1) + fib(x-2): x;
fib = fib.Memoize();

Что, на мой взгляд, гораздо симпатичнее) Производительность, конечно, надо сравнивать, но не 4 секунды на fib(40), это уж точно.

А за то, что автор, ударился в такие исследования, даже не упомянув такую возможность языка, как мемоизация, на мой взгляд — нереспект(
0
blanabrother #
Алгоритм однозначно хорош. Но я для себя решил попробовать, что же такое «идеальный алгоритм» возведения в целочисленную степень.
Написал его реализацию на С# и сравнил с Math.Pow — два метода дают абсолютно одинаковый результат при любых значениях степени и самого числа, возводимого в степень. Сдается, что в Microsoft работают неглупые парни и знают, что возводить в степень не стоит простым циклом.
0
blanabrother #
Одинаковый результат не в смысле значения, а в смысле производительности. Т.е. тест заключался в возведении в степень массива данных.
Короче говоря, время работы Math.Pow не дольше.
0
mezastel #
У вас что-то кардинально не так. На целых числах вы должны получить совершенно разные результаты. Почитайте ветку обсуждений, тут кажется это уже затронули.
0
blanabrother #
Целые не пробовал. Надо посмотреть.
Я как бы не пытаюсь каким-либо образом перекритиковать.
Просто алгоритм действительно очень оптимизирован и кажется его истоки далеко в математике. Из-за этого решил его написать по описанию автора и сравнить с существующими средствами.
Кроме этого, если почитать документацию к процессорам, то там можно найти соответствующие опкоды. Процессоры давно умеют возводить в степень аппаратно, плюс Math.Pow — это extern функция, как многие другие из этого класса. Возможно, процессоры используют некое подобие описанного выше «идеального алгоритма»

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.