Произвольно-точная арифметика Объяснение - PullRequest
87 голосов
/ 02 августа 2009

Я пытаюсь выучить C и столкнулся с невозможностью работать с действительно большими числами (т. Е. 100 цифрами, 1000 цифрами и т. Д.). Я знаю, что для этого существуют библиотеки, но я хочу попытаться реализовать это самостоятельно.

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

Ответы [ 8 ]

156 голосов
/ 02 августа 2009

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

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

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

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

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

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

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

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

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

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

Некоторые хитрости, на которые стоит обратить внимание.

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

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

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

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) более поддаются обсуждению потенциальных изменений - они кажутся более дружественными для разработчиков.

8 голосов
/ 02 августа 2009

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

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

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

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

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

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

6 голосов
/ 02 августа 2009

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

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

4 голосов
/ 02 августа 2009

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

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

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

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

в соответствии с вашей архитектурой.

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

3 голосов
/ 17 сентября 2012

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

@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},
}
3 голосов
/ 02 августа 2009

Вы можете сделать это со средним уровнем математики. Хотя в реальности используются более продвинутые алгоритмы. Так, например, добавить два 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 - отличная библиотека, если вы хотите учиться. Он построен с использованием C ++. Приведенный выше пример был глупым, но в общем случае сложение и вычитание выполняются!

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

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

1 голос
/ 02 августа 2009

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

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

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

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

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

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

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

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

0 голосов
/ 21 марта 2012

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

Я реализовал «Добавить» и «Умножить» и использовал это в качестве примера степени.

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

Кодовый фрагмент

// 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);
}
...