Арифметична довільна точність Пояснення


92

Я намагаюся вивчити C і зіткнувся з неможливістю працювати з ДІЙСНО великими числами (тобто 100 цифрами, 1000 цифрами тощо). Я усвідомлюю, що для цього існують бібліотеки, але я хочу спробувати реалізувати це сам.

Я просто хочу знати, чи є чи може хтось надати дуже детальне, дефіцитне пояснення арифметики довільної точності.

Відповіді:


162

Все залежить від достатнього зберігання та алгоритмів, щоб розглядати числа як менші частини. Припустимо, у вас є компілятор, в якому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 перенесення

Фактично це те, як додавання, як правило, працює на рівні бітів у вашому процесорі.

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

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

Деякі хитрощі, на які слід стежити.

1 / Під час додавання чи множення чисел заздалегідь виділіть максимум необхідного місця, а потім зменште, якщо виявите, що його занадто багато. Наприклад, додавання двох 100- "цифр" (де цифра - це 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 цифр по черзі (назад), а потім використання додавання для обчислення значення, яке потрібно додати до результату (спочатку нуль).

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 Multiple Precision Arithmetic - це набагато краще та швидше, ніж мої власні бібліотеки.

У нього є досить прикрі помилки в тому, що він просто вийде, якщо в нього закінчиться пам’ять (на мій погляд, досить фатальний недолік бібліотеки загального призначення), але, якщо ви зможете пройти мимо цього, це досить добре в тому, що він робить.

Якщо ви не можете використовувати його з ліцензійних причин (або тому, що не хочете, щоб програма просто виходила без видимих ​​причин), ви можете принаймні отримати алгоритми звідти для інтеграції у власний код.

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


14
Я думаю, ви висвітлили: "Я просто хочу знати, чи хтось має чи може надати дуже детальне, уточнене пояснення арифметики довільної точності" ДУЖЕ добре
Грант Пітерс,

Одне наступне запитання: чи можна встановити / виявити перенесення та переповнення без доступу до машинного коду?
SasQ

8

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

Наприклад, множення. Наївно ви можете подумати про метод "школяра", тобто написати одне число над іншим, а потім робити довге множення, як ви навчилися в школі. приклад:

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

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

І це лише для цілих чисел. Коли ви залучаєтеся до більш складних функцій на якомусь типі реального подання числа (log, sqrt, exp тощо), все стає ще більш складним.

Якщо ви хочете отримати теоретичні знання, настійно рекомендую прочитати перший розділ книги Япа "Основні проблеми алгоритмічної алгебри" . Як уже зазначалося, бібліотека gmp bignum - відмінна бібліотека. Для дійсних чисел я використовував mpfr і мені сподобався.


1
Мене цікавить частина про "використовувати або дискретне перетворення Фур'є, або числове перетворення, щоб перетворити це на по суті операцію O (n ln (n))" - як це працює? Просто посилання було б
непоганим

1
@detly: множення поліномів те саме, що і згортка, знайти інформацію про використання ШПФ для швидкої згортки повинно бути легко. Будь-яка система числення є багаточленом, де цифри є коефіцієнтами, а основа - основою. Звичайно, вам доведеться подбати про носіння, щоб уникнути перевищення діапазону цифр.
Ben Voigt

6

Не винаходите колесо заново: воно може виявитися квадратним!

Використовуйте сторонню бібліотеку, таку як GNU MP , яка перевірена.


4
Якщо ви хочете вивчити C, я б поставив ваші приціли трохи нижче. Впровадження бібліотеки bignum є нетривіальним з усіх видів тонких причин, які спонукають учня
Mitch Wheat,

3
Бібліотека третьої сторони: погоджено, але GMP має проблеми з ліцензуванням (LGPL, хоча ефективно діє як GPL, оскільки дуже складно робити високопродуктивну математику за допомогою LGPL-сумісного інтерфейсу).
Jason S

Посилання на приємну Футураму (навмисне?)
Грант Пітерс,

7
GNU MP безумовно закликає abort()до помилок розподілу, які неодмінно трапляються з певними шалено великими обчисленнями. Це неприйнятна поведінка для бібліотеки і достатня причина для написання власного коду довільної точності.
R .. GitHub СТОП ДОПОМОГАЙ ЛЕД

Я маю там погодитися з R. Бібліотека загального призначення, яка просто витягує килим з-під вашої програми, коли пам’яті закінчується, непростимо. Я б хотів, щоб вони пожертвували якоюсь швидкістю заради безпеки / відновлення.
paxdiablo

4

Ви робите це в основному так само, як і з олівцем та папером ...

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

Зазвичай ви будете використовувати як основну одиницю обчислення

  • байти, що містять з 0-99 або 0-255
  • 16-бітові слова, що містять в'януть 0-9999 або 0-65536
  • 32-розрядні слова, що містять ...
  • ...

як диктує ваша архітектура.

Вибір двійкової або десяткової бази залежить від ваших бажань щодо максимальної космічної ефективності, людської читабельності та наявності відсутності на вашому чіпі математичної підтримки з двійковим кодом (BCD).


3

Ви можете зробити це на середньому рівні математики. Хоча насправді використовуються більш досконалі алгоритми. Так, наприклад, щоб додати два 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зберегти результат множення.

Як сказав Мітч , здійснити це далеко не просто завдання! Якщо ви знаєте C ++, я рекомендую вам поглянути на TTMath.


Використання масивів мені справді спало на думку, але я шукаю щось ще більш загальне. Дякуємо за відповідь!
ТТ.

2
Хм ... ім'я запитувача та назва бібліотеки не може бути випадковістю, чи не так? ;)
Джон Й

Лол, я цього не помітив! Я хотів би, щоб справді TTMath був моїм :) До речі, ось одне з моїх питань на цю тему:
AraK


3

Одне з остаточних посилань (ІМХО) - 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},
}

1

Якщо припустити, що ви хочете написати великий цілочисельний код самостійно, це може бути напрочуд просто, якщо говорити як хтось, хто робив це недавно (хоча в MATLAB.) Ось декілька прийомів, якими я скористався:

  • Я зберігав кожну окрему десяткову цифру як подвійне число. Це робить багато операцій простими, особливо вихідними. Хоча це займає більше місця, ніж ви могли б побажати, пам’ять тут дешева, і це робить множення дуже ефективним, якщо ви можете ефективно згорнути пару векторів. Крім того, ви можете зберегти кілька подрядних цифр, але пам’ятайте, що згортка для множення може спричинити числові проблеми для дуже великих чисел.

  • Зберігайте розрядний біт окремо.

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

  • Множення пари чисел найкраще виконувати як згортку з подальшим кроком перенесення, принаймні, якщо у вас є швидкий код згортки.

  • Навіть коли ви зберігаєте числа як рядок окремих десяткових цифр, поділ (також mod / rem ops) можна зробити, щоб отримати в результаті приблизно 13 десяткових цифр за раз. Це набагато ефективніше, ніж розділення, яке одночасно працює лише на 1 десяткову цифру.

  • Щоб обчислити цілочисельну ступінь цілого числа, обчисліть двійкове представлення показника ступеня. Потім за допомогою повторних операцій квадратування обчислюйте потужності за необхідності.

  • Багато операцій (факторинг, тести первинності тощо) отримають вигоду від операції Powermod. Тобто, коли ви обчислюєте mod (a ^ p, N), зменшуйте результат mod N на кожному кроці піднесення до степені, де p було виражено у двійковій формі. Спочатку не обчислюйте a ^ p, а потім намагайтеся зменшити його mod N.


1
Якщо ви зберігаєте окремі цифри, а не base-10 ^ 9 або base-2 ^ 32 або щось подібне, всі ваші фантастичні речі, що складаються із множення для множення, - це просто марнотратство. Big-O є досить безглуздим , коли ваша постійна , що погано ...
R .. GitHub СТОП ДОПОМАГАТИ ICE

0

Ось простий (наївний) приклад, який я зробив у 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);
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.