Задний план:
Меня «затащили» на этот вопрос: Выражение закрытой формы Фибоначчи в Haskell
когда автор сначала использовал теги многих других языков, но позже сосредоточился на вопросе Haskell. К сожалению, у меня нет опыта работы с Haskell, поэтому я не мог участвовать в этом вопросе. Однако один из ответов привлек мое внимание, куда повернулся ответчик это в чисто целочисленную математическую задачу. Это звучало для меня потрясающе, поэтому мне пришлось выяснить, как это работает, и сравнить это с рекурсивной реализацией Фибоначчи, чтобы увидеть, насколько она точна. У меня есть ощущение, что если бы я просто вспомнил соответствующую математику, связанную с иррациональными числами, я мог бы решить все сам (но я этого не делаю). Так что первым шагом для меня было портировать его на язык, с которым я знаком. В данном случае я делаю C#.
К счастью, я не совсем в темноте. У меня большой опыт работы с другим функциональным языком (OCaml), поэтому многое из этого показалось мне несколько знакомым. Начав с преобразования, все казалось простым, так как он фактически определял новый числовой тип для облегчения вычислений. Однако я столкнулся с парой препятствий в переводе, и у меня возникли проблемы с его завершением. Я получаю совершенно неправильные результаты.
Анализ:
Вот код, который я перевожу:
data Ext = Ext !Integer !Integer
deriving (Eq, Show)
instance Num Ext where
fromInteger a = Ext a 0
negate (Ext a b) = Ext (-a) (-b)
(Ext a b) + (Ext c d) = Ext (a+c) (b+d)
(Ext a b) * (Ext c d) = Ext (a*c + 5*b*d) (a*d + b*c) -- easy to work out on paper
-- remaining instance methods are not needed
fib n = divide $ twoPhi^n - (2-twoPhi)^n
where twoPhi = Ext 1 1
divide (Ext 0 b) = b `div` 2^n -- effectively divides by 2^n * sqrt 5
Итак, основываясь на моем исследовании и том, что я могу вывести (поправьте меня, если я где-то ошибаюсь), первая часть объявляет тип Ext
с конструктором, который будет иметь два параметра Integer
(и, я думаю, унаследует типы/модули Eq
и Show
) .
Далее следует реализация Ext
, которая «происходит» от Num
. fromInteger
выполняет преобразование из Integer
. negate
— это унарное отрицание, а затем есть двоичные операторы сложения и умножения.
Последняя часть — это фактическая реализация Фибоначчи.
Вопросы:
В ответе Хаммар (ответчик) упоминает, что возведение в степень обрабатывается реализацией по умолчанию в Num
. Но что это значит и как это на самом деле применяется к этому типу? Есть ли неявное числовое «поле», которое мне не хватает? Применяет ли он просто возведение в степень к каждому соответствующему числу, которое оно содержит? Я предполагаю, что это делает последнее и в итоге получается этот код С#:
public static Ext operator ^(Ext x, int p) // "exponent"
{
// just apply across both parts of Ext?
return new Ext(BigInt.Pow(x.a, p), BigInt.Pow(x.b, p));
// Ext (a^p) (b^p)
}
Однако это противоречит тому, как я понимаю, зачем нужен negate
, он не понадобится, если это действительно произойдет.
Теперь суть кода. Я прочитал первую часть
divide $ twoPhi^n - (2-twoPhi)^n
как:
разделить результат следующего выражения: twoPhi^n - (2-twoPhi)^n.
Довольно просто. Возведите twoPhi
в n
степень. Вычтите из этого результат остальных. Здесь мы делаем двоичное вычитание, но реализовали только унарное отрицание. Или мы не сделали? Или можно ли подразумевать двоичное вычитание, потому что его можно составить, комбинируя сложение и отрицание (что у нас есть)? Я предполагаю последнее. И это облегчает мою неуверенность в отрицании.
Последняя часть — это фактическое деление:
divide (Ext 0 b) = b `div` 2^n
. Здесь две проблемы. Из того, что я нашел, нет оператора деления, только функция `div`
. Так что мне просто нужно разделить числа здесь. Это правильно? Или есть оператор деления, но отдельная функция `div`
, которая делает что-то особенное?
Я не уверен, как интерпретировать начало строки. Это просто совпадение с образцом? Другими словами, будет ли это применяться только в том случае, если первым параметром будет 0
? Каким был бы результат, если бы он не совпадал (первое было ненулевым)? Или я должен интерпретировать это, поскольку мы не заботимся о первом параметре и применяем функцию безоговорочно? Это кажется самым большим препятствием, и использование любой интерпретации по-прежнему дает неверные результаты.
Я сделал какие-либо неправильные предположения в любом месте? Или все в порядке и я просто неправильно реализовал C#?
Код:
Вот (нерабочий) перевод и полный исходный код (включая тесты), поэтому далеко, если кому интересно.
// code removed to keep post size down
// full source still available through link above
Прогресс:
Итак, глядя на ответы и комментарии до сих пор, я думаю, что знаю, куда идти дальше и почему.
Возведение в степень просто нужно было сделать, как обычно, умножить p
раз, учитывая, что мы реализовали операцию умножения. Мне никогда не приходило в голову, что мы должны делать то, что нам всегда говорили на уроках математики. Подразумеваемое вычитание из сложения и отрицания также является довольно удобной функцией.
Также заметил опечатку в моей реализации. Я добавил, когда я должен был умножить.
// (Ext a b) * (Ext c d) = Ext (a*c + 5*b*d) (a*d + b*c)
public static Ext operator *(Ext x, Ext y)
{
return new Ext(x.a * y.a + 5*x.b*y.b, x.a*y.b + x.b*y.a);
// ^ oops!
}
Вывод:
Итак, теперь это завершено. Я реализовал только основные операторы и немного переименовал их. Названы аналогично комплексным числам. Пока что соответствует рекурсивной реализации даже при очень больших входных данных. Вот окончательный код.
static readonly Complicated TWO_PHI = new Complicated(1, 1);
static BigInt Fib_x(int n)
{
var x = Complicated.Pow(TWO_PHI, n) - Complicated.Pow(2 - TWO_PHI, n);
System.Diagnostics.Debug.Assert(x.Real == 0);
return x.Bogus / BigInt.Pow(2, n);
}
struct Complicated
{
private BigInt real;
private BigInt bogus;
public Complicated(BigInt real, BigInt bogus)
{
this.real = real;
this.bogus = bogus;
}
public BigInt Real { get { return real; } }
public BigInt Bogus { get { return bogus; } }
public static Complicated Pow(Complicated value, int exponent)
{
if (exponent < 0)
throw new ArgumentException(
"only non-negative exponents supported",
"exponent");
Complicated result = 1;
Complicated factor = value;
for (int mask = exponent; mask != 0; mask >>= 1)
{
if ((mask & 0x1) != 0)
result *= factor;
factor *= factor;
}
return result;
}
public static implicit operator Complicated(int real)
{
return new Complicated(real, 0);
}
public static Complicated operator -(Complicated l, Complicated r)
{
var real = l.real - r.real;
var bogus = l.bogus - r.bogus;
return new Complicated(real, bogus);
}
public static Complicated operator *(Complicated l, Complicated r)
{
var real = l.real * r.real + 5 * l.bogus * r.bogus;
var bogus = l.real * r.bogus + l.bogus * r.real;
return new Complicated(real, bogus);
}
}
А вот и полностью обновленный источник.
^
означает «побитовое XOR» в C#. Вы можете перегрузить его, чтобы он означал возведение в степень, но в культуре C# это будет считаться запутанным и нежелательным, поскольку это неожиданное несоответствие. Вместо этого рассмотрите возможность объявления методаPow
. - person Timwi   schedule 21.05.2011