Pull to refresh

Comments 54

Мне кажется что C# не оптимизирует хвостовую рекурсию (хотя я могу ошибаться).
Интереснее было бы для сравнения привести вариант на F# (и сравнить скорость).
На сволько мне известно, на уровне MSIL хвостовая рекурсия поддерживается, но сейчас C# компилятор это не использует и ситуацию вроде как обещали исправить с выходом .NET 4.
Забыли еще про способ с возведением в степень золотого сечения.
Он менее эффективен(целые перемножаются быстрее) и дает неточный ответ, но вспомнить про него надо было.
Вспоминать про него необязательно, так как на самоме деле описанный способ можно слегка переделать и получить то, что Вы говорите, а именно: если расписать матрицу [[1,1],[1,0]] в жордановой форме, то там получится диагональная матрица, у которой на месте (1,1) как раз золотое сечение. Ну и степень будет соответственная.
Но, как правильно отмечено, перемножать и складывать единички получается быстрее, чем иррациональное число в степень возводить.
Или на месте (2,2), это смотря как жорданку расписывать.
[занудство]
Все же для чисел фибоначчи с номером в районе 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 (чисто интуитивно, возможно я и ошибаюсь).
[/занудство]
Вы неправы.
Ваш алгоритм работает за O(n)
А описаный в статье за O(lg(n))
Даже с учетом медленного умножения при n > 100 алгоритм из статьи будет быстрее.
специально написал простой код для замера производительности.
он становится ощутимо быстрее (в 2 раза) при n > 70 с использованием обычных int'ов.
если использовать long'и — при n == 120 они равны.
сейчас попробую померять время с длинной арифметикой в фреймворке 4.0
да, согласен. с длинной арифметикой их .net 4.0 они равны при n == 100
Если сравнивать с рекурсивным применением, то матрица дает прирост примерно при N>10. Конечно реализация с кэшированием намного быстрее на начальном этапе, и мы раньге получим overflow чем матрица начнет обгонять эту реализацию по производительности.

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

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.


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


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

Короче, убивал бы за подобные решения «в лоб» :).
Так преподы не настолько продвинуты — они вам за такое как раз 5 поставят, а если начнете «мудрить» они обидятся что де непонятно и поставят пару за то что «умничаете».
Ну, у меня были в основном именно такие…
Ну вот, опять началось.
зы Хотя уж кто бы говорил.
Хм, не вижу, честно говоря, интереса играться с рекурсией, как приёмом в программировании, с числами Фибоначчи, для которых, вообще говоря, существует формула Бине для вычисления чисел Фибоначчи без рекурсии.
Когда научитесь быстро возводить в степень иррациональные числа, обращайтесь.
Показатель-то целый — тот же самый алгоритм работает. Формула Бине может дать более быстрый результат, чем решение в статье.

Впрочем, если мне нужны быстро и точно числа Фибоначчи, я лучше lookup-table заведу. Статья-то как бы не об этом :-)
>>Формула Бине может дать более быстрый результат
Быстрый, но не точный. Как числа с плавающей точкой в компутере хранятся знаете?
А вы формулу-то поглядели или просто так пишете?
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. Извините за перл, но вы легко проведёте тест на своём любимом языке.
Поставьте вместо 60 70 и увидите про что я говорил
О, вот оно. Точно вы большую степень золотого сечения не посчитаете, символьно это вообще повеситься можно. А тут раз-раз, символьно нолики и единички, круто.
Ну можно организовать числа с плавающей точкой высокой точности или гибрид double/long, где double хранит только дробную часть числа (не знаю, кто придумал, но увидел у П.Митричева) для не очень больших значений N.
Только игра не стоит свеч, лучше всё-таки целочисленно.
public static mtx2x2 IntPower(mtx2x2 x, short power)
{
  if (power < 2) return x;

Ещё занудство: любая матрица в нулевой степени даст единичную матрицу. У вас не так =)
Внезапно! Я долго пытался вспомнить, что это за топик и про что был мой комментарий, когда мне пришло уведомление на почту :-)
это же надо, не читали дальше окончания заголовка, а все же не поленились зайти внутрь топика и оставить комментарий, чтобы сообщить об этом факте всему миру. ваше упорство достойно лучшего применения ;)
Ну, на Хабре часто статьи связанные с Microsoft минусуют за 5 секунд после публикации. Тут просто человек откровеннее среднего :)
Иногда кажется, что даже не через 5 после, а за 5 секунд _до_.
упс, нужно чаще обновлять страницу :(
Факториалы, числа Фибоначчи, я красотой рекурсии проникся, когда увидел рекурсивную сборку ханойской башни. Вот тогда я и захотел понять что это. Алгоритм, кажется, был в «Науке и жизнь» за какой-то бородатый год.
для каждого умножения матриц потребуется 8 умножений.
Лучше использовать эти формулы:
F(2n)=F(n+1)^2-F(n-1)^2
F(2n+1)=F(n+1)^2+F(n)^2
Вот это вы очень кстати сказали. Ведь у матрицы a12=a21 походу, а лишние вычисления ни к чему.

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

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

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

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

Articles