Арифметика с произвольной точностью Объяснение

Я пытаюсь изучить C и столкнулся с неспособностью работать с ДЕЙСТВИТЕЛЬНО большими числами (например, 100 цифр, 1000 цифр и т.д.). Я знаю, что существуют библиотеки для этого, но я хочу попытаться реализовать его сам.

Я просто хочу знать, имеет ли кто-нибудь или может предоставить очень подробное, ошарашенное объяснение арифметики с произвольной точностью.

Ответ 1

Все дело в адекватном хранении и алгоритмах для обработки чисел как небольших частей. Предположим, что у вас есть компилятор, в котором int может быть только от 0 до 99, и вы хотите обрабатывать номера до 999999 (мы будем беспокоиться только о положительных числах здесь, чтобы это было просто).

Вы делаете это, задавая каждый номер три int и используя те же правила, которые вы (должны были) получить в начальной школе для сложения, вычитания и других основных операций.

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

Дополнение, например: 123456 + 78:

12 34 56
      78
-- -- --
12 35 34

Работа с наименее значимым концом:

  • начальный перенос = 0.
  • 56 + 78 + 0 carry = 134 = 34 с 1 переносом
  • 34 + 00 + 1 carry = 35 = 35 с 0 переносить
  • 12 + 00 + 0 carry = 12 = 12 с 0 переносить

Это, по сути, то, как дополнение обычно работает на уровне бит внутри вашего процессора.

Вычитание аналогично (с использованием вычитания базового типа и заимствования вместо переноса), умножение может быть выполнено с повторными добавлениями (очень медленными) или кросс-продуктами (быстрее), а деление сложнее, но может быть выполнено сдвигом и вычитанием из числа вовлеченных (длинное разделение, которое вы узнали бы в детстве).

Я на самом деле писал библиотеки, чтобы делать подобные вещи, используя максимальные значения десяти, которые могут быть помещены в целое число при квадрате (чтобы предотвратить переполнение при объединении двух int вместе, например, 16-разрядный int ограничивается 0 до 99, чтобы генерировать 9,801 (< 32,768) при квадрате или 32-бит int с использованием от 0 до 9999 для генерации 99 980 001 (< 2,147,483,648)), что значительно облегчило алгоритмы.

Некоторые трюки, на которые следует обратить внимание.

1/При добавлении или умножении чисел предварительно назначьте необходимое пространство, а затем уменьшите его, если найдете его слишком много. Например, добавление двух цифр "цифра" (цифра - int) никогда не даст вам более 101 цифры. Умножьте 12-значное число на 3-значное число никогда не будет генерировать более 15 цифр (добавьте количество цифр).

2/Для дополнительной скорости нормализуйте (уменьшите требуемую память) цифры только в том случае, если это абсолютно необходимо - в моей библиотеке это было как отдельный вызов, поэтому пользователь может решить проблему между скоростью и хранением.

3/Добавление положительного и отрицательного числа является вычитанием, а вычитание отрицательного числа совпадает с добавлением эквивалентного положительного. Вы можете сэкономить немало кода, если методы добавления и вычитания вызовут друг друга после настройки значков.

4/Избегайте вычитания больших чисел из маленьких, так как вы неизменно заканчиваете цифрами вроде:

         10
         11-
-- -- -- --
99 99 99 99 (and you still have a borrow).

Вместо этого вычтите 10 из 11, затем отрицайте это:

11
10-
--
 1 (then negate to get -1).

Вот комментарии (превращенные в текст) из одной из библиотек, которые я должен был сделать для этого. Сам код, к сожалению, защищен авторским правом, но вы можете выбрать достаточную информацию для обработки четырех основных операций. Предположим, что -a и -b представляют отрицательные числа, а a и b - нулевые или положительные числа.

Для добавления, если знаки различны, используйте вычитание отрицания:

-a +  b becomes b - a
 a + -b becomes a - b

Для вычитания, если знаки различны, используйте добавление отрицания:

 a - -b becomes   a + b
-a -  b becomes -(a + b)

Также специальная обработка, чтобы мы вычитали небольшие числа из больших:

small - big becomes -(big - small)

Умножение использует математику начального уровня следующим образом:

475(a) x 32(b) = 475 x (30 + 2)
               = 475 x 30 + 475 x 2
               = 4750 x 3 + 475 x 2
               = 4750 + 4750 + 4750 + 475 + 475

Способ, которым это достигается, заключается в извлечении каждой из цифр по 32 за раз (назад), а затем с помощью add для вычисления значения, добавляемого к результату (изначально нуль).

Операции

ShiftLeft и ShiftRight используются для быстрого умножения или деления значения LongInt на значение обертки (10 для "реальной" математики). В приведенном выше примере мы добавим 475 к нулю 2 раза (последняя цифра 32), чтобы получить 950 (результат = 0 + 950 = 950).

Затем мы оставили сдвиг 475, чтобы получить 4750 и правый сдвиг 32, чтобы получить 3. Добавьте 4750 к нулю 3 раза, чтобы получить 14250, затем добавьте к результату 950, чтобы получить 15200.

Левая смена 4750 для получения 47500, сдвиг вправо 3, чтобы получить 0. Так как правый сдвиг 32 теперь равен нулю, мы закончили и, фактически, 475 x 32 составляет 15200.

Дивизия также сложна, но основана на ранней арифметике (метод "gazinta" для "переходит в" ). Рассмотрим следующее длинное деление для 12345 / 27:

       457
   +-------
27 | 12345    27 is larger than 1 or 12 so we first use 123.
     108      27 goes into 123 4 times, 4 x 27 = 108, 123 - 108 = 15.
     ---
      154     Bring down 4.
      135     27 goes into 154 5 times, 5 x 27 = 135, 154 - 135 = 19.
      ---
       195    Bring down 5.
       189    27 goes into 195 7 times, 7 x 27 = 189, 195 - 189 = 6.
       ---
         6    Nothing more to bring down, so stop.

Следовательно, 12345 / 27 есть 457 с остатком 6. Убедитесь, что:

  457 x 27 + 6
= 12339    + 6
= 12345

Это реализовано с помощью переменной-вытягивания (изначально нулевой), чтобы свести сегменты по 12345 по одному, пока она не станет больше или равна 27.

Тогда мы просто вычитаем 27 из этого, пока не получим ниже 27 - количество вычитаний - это сегмент, добавленный в верхнюю строку.

Если сегментов больше нет, мы получим наш результат.


Имейте в виду, что это довольно простые алгоритмы. Есть гораздо лучшие способы выполнения сложной арифметики, если ваши номера будут особенно большими. Вы можете посмотреть что-то вроде Библиотека многоточечной арифметики GNU - она ​​значительно лучше и быстрее, чем мои собственные библиотеки.

У него есть довольно неудачная ошибка в том, что он просто выйдет, если у него закончится память (довольно фатальный недостаток для библиотеки общего назначения, на мой взгляд), но, если вы можете смотреть мимо этого, это довольно хорошо, он делает.

Если вы не можете использовать его по причинам лицензирования (или из-за того, что вы не хотите, чтобы приложение просто выходило без видимых причин), вы могли бы, по крайней мере, получить оттуда алгоритмы для интеграции в свой собственный код.

Я также обнаружил, что те, кто находится на MPIR (вилка GMP), более поддаются обсуждению потенциальных изменений - они кажутся более дружественной для разработчиков связью.

Ответ 2

Не изобретайте велосипед: он может оказаться квадратным!

Используйте стороннюю библиотеку, такую ​​как GNU MP, которая проверена и проверена.

Ответ 3

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

Например, умножение. Наивно, вы можете подумать о методе "школьника", т.е. Написать одно число выше другого, а затем делать длинное умножение, как вы учились в школе. Пример:

      123
    x  34
    -----
      492
+    3690
---------
     4182

но этот метод чрезвычайно медленный (O (n ^ 2), n - число цифр). Вместо этого современные пакеты bignum используют либо дискретное преобразование Фурье, либо числовое преобразование, чтобы превратить это в операцию O (n ln (n)).

И это просто для целых чисел. Когда вы переходите к более сложным функциям для определенного типа реального представления числа (log, sqrt, exp и т.д.), Все становится еще сложнее.

Если вам нужен какой-то теоретический фон, я настоятельно рекомендую прочитать первую главу книги Yap, "Основные проблемы алгоритмической алгебры" . Как уже упоминалось, библиотека gmp bignum - отличная библиотека. Для реальных чисел я использовал mpfr и мне понравилось.

Ответ 4

Вы можете сделать это с высшим школьным уровнем математики. Хотя в действительности используются более сложные алгоритмы. Например, чтобы добавить два 1024-байтных номера:

unsigned char first[1024], second[1024], result[1025];
unsigned char carry = 0;
unsigned int  sum   = 0;

for(size_t i = 0; i < 1024; i++)
{
    sum = first[i] + second[i] + carry;
    carry = sum - 255;
}
Результат

должен быть больше one place в случае добавления, чтобы позаботиться о максимальных значениях. Посмотрите на это:

9
   +
9
----
18

TTMath - отличная библиотека, если вы хотите учиться. Он построен с использованием С++. Вышеприведенный пример был глупым, но это то, как сложение и вычитание выполняются в целом!

Хорошая ссылка на тему вычислительная сложность математических операций. Он сообщает вам, сколько места требуется для каждой операции, которую вы хотите реализовать. Например, если у вас есть два номера N-digit, вам нужно 2N digits сохранить результат умножения.

Как сказал Mitch, это непростая задача! Я рекомендую вам взглянуть на TTMath, если вы знаете С++.

Ответ 5

Вы делаете это в основном так же, как с карандашом и бумагой...

  • Число должно быть представлено в буфере (массиве), способном принимать произвольный размер (что означает использование malloc и realloc) по мере необходимости
  • вы максимально используете базовую арифметику с использованием поддерживаемых языков структур и занимаетесь переносом и перемещением точки счисления вручную
  • вы просматриваете текстовые аналитические тексты, чтобы найти эффективные аргументы для работы более сложной функцией.
  • вы только реализуете столько, сколько вам нужно.

Обычно вы будете использовать в качестве базовой единицы вычисления

  • байты, содержащие 0-99 или 0-255
  • 16-битные слова contaning wither 0-9999 или 0--65536
  • 32-битные слова, содержащие...
  • ...

как продиктовано вашей архитектурой.

Выбор двоичной или десятичной базы зависит от ваших желаний для максимальной эффективности пространства, удобочитаемости человека и отсутствия математической поддержки двоично-кодированного десятичного (BCD) на вашем чипе.

Ответ 6

Одной из основных ссылок (ИМХО) является Том II Кнута TAOCP. Он объясняет множество алгоритмов для представления чисел и арифметических операций над этими представлениями.

@Book{Knuth:taocp:2,
   author    = {Knuth, Donald E.},
   title     = {The Art of Computer Programming},
   volume    = {2: Seminumerical Algorithms, second edition},
   year      = {1981},
   publisher = {\Range{Addison}{Wesley}},
   isbn      = {0-201-03822-6},
}

Ответ 7

Предполагая, что вы хотите написать большой целочисленный код самостоятельно, это может быть удивительно просто сделать, как говорят кто-то, кто делал это недавно (хотя в MATLAB.) Вот несколько трюков, которые я использовал:

  • Я сохранил каждую отдельную десятичную цифру в виде двойного числа. Это делает многие операции простыми, особенно выходными. Хотя для этого требуется больше памяти, чем вы могли бы захотеть, здесь дешевая память, и это делает умножение очень эффективным, если вы можете эффективно сверлить пару векторов. Кроме того, вы можете хранить несколько десятичных цифр в двойном, но будьте осторожны, тогда свертка для выполнения умножения может вызвать численные проблемы на очень больших числах.

  • Храните знаковый бит отдельно.

  • Добавление двух чисел в основном связано с добавлением цифр, а затем проверить перенос на каждом шаге.

  • Умножение пары чисел лучше всего выполнять как свертку, за которой следует шаг переноса, по крайней мере, если у вас есть быстрый код свертки при нажатии.

  • Даже если вы храните цифры в виде строки отдельных десятичных цифр, деление (также mod/rem ops) может быть выполнено, чтобы получить примерно 13 десятичных цифр за раз в результате. Это намного эффективнее, чем деление, которое работает только с 1 десятичной цифрой за раз.

  • Чтобы вычислить целочисленную степень целого числа, вычислите двоичное представление показателя. Затем используйте операции повторного квадратичного вычисления, чтобы вычислить мощности по мере необходимости.

  • Многие операции (факторинг, тесты на прочность и т.д.) будут полезны при работе с электромотором. То есть, когда вы вычисляете mod (a ^ p, N), уменьшайте результат mod N на каждом шаге экспоненциации, где p выражен в двоичной форме. Сначала не вычисляйте a ^ p, а затем попытайтесь уменьшить его mod N.

Ответ 8

Вот простой (наивный) пример, который я сделал в PHP.

Я реализовал "Добавить" и "Умножить" и использовал это для примера экспоненты.

http://adevsoft.com/simple-php-arbitrary-precision-integer-big-num-example/

Код snip

// Add two big integers
function ba($a, $b)
{
    if( $a === "0" ) return $b;
    else if( $b === "0") return $a;

    $aa = str_split(strrev(strlen($a)>1?ltrim($a,"0"):$a), 9);
    $bb = str_split(strrev(strlen($b)>1?ltrim($b,"0"):$b), 9);
    $rr = Array();

    $maxC = max(Array(count($aa), count($bb)));
    $aa = array_pad(array_map("strrev", $aa),$maxC+1,"0");
    $bb = array_pad(array_map("strrev", $bb),$maxC+1,"0");

    for( $i=0; $i<=$maxC; $i++ )
    {
        $t = str_pad((string) ($aa[$i] + $bb[$i]), 9, "0", STR_PAD_LEFT);

        if( strlen($t) > 9 )
        {
            $aa[$i+1] = ba($aa[$i+1], substr($t,0,1));
            $t = substr($t, 1);
        }

        array_unshift($rr, $t);
     }

     return implode($rr);
}