Aritmetica a precisione arbitraria Spiegazione


92

Sto cercando di imparare il C e mi sono imbattuto nell'incapacità di lavorare con numeri VERAMENTE grandi (ad esempio, 100 cifre, 1000 cifre, ecc.). Sono consapevole che esistono librerie per farlo, ma voglio tentare di implementarlo da solo.

Voglio solo sapere se qualcuno ha o può fornire una spiegazione molto dettagliata e stupida dell'aritmetica a precisione arbitraria.

Risposte:


162

È tutta una questione di archiviazione e algoritmi adeguati per trattare i numeri come parti più piccole. Supponiamo che tu abbia un compilatore in cui an intpuò essere solo da 0 a 99 e vuoi gestire numeri fino a 999999 (ci preoccuperemo solo dei numeri positivi qui per mantenerlo semplice).

Lo fai dando a ogni numero tre se intusando le stesse regole che (dovresti) aver imparato alla scuola primaria per l'addizione, la sottrazione e le altre operazioni di base.

In una libreria di precisione arbitraria, non esiste un limite fisso al numero di tipi di base utilizzati per rappresentare i nostri numeri, qualunque sia la memoria che può contenere.

Aggiunta ad esempio 123456 + 78:

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

Lavorando dall'estremità meno significativa:

  • riporto iniziale = 0.
  • 56 + 78 + 0 riporto = 134 = 34 con 1 riporto
  • 34 + 00 + 1 riporto = 35 = 35 con 0 riporto
  • 12 + 00 + 0 riporto = 12 = 12 con 0 riporto

Questo è, infatti, il modo in cui generalmente funziona l'addizione a livello di bit all'interno della CPU.

La sottrazione è simile (usando la sottrazione del tipo di base e prendere in prestito invece di riporto), la moltiplicazione può essere eseguita con addizioni ripetute (molto lente) o prodotti incrociati (più veloci) e la divisione è più complicata ma può essere eseguita spostando e sottrando i numeri coinvolti (la lunga divisione che avresti imparato da bambino).

In realtà ho scritto librerie per fare questo genere di cose usando le potenze massime di dieci che possono essere inserite in un numero intero quando è al quadrato (per evitare overflow quando si moltiplicano due ints insieme, ad esempio un 16 bit intlimitato da 0 a 99 a generare 9.801 (<32.768) al quadrato o 32 bit intutilizzando da 0 a 9.999 per generare 99.980.001 (<2.147.483.648)) che ha notevolmente facilitato gli algoritmi.

Alcuni trucchi a cui prestare attenzione.

1 / Quando si aggiungono o si moltiplicano numeri, pre-allocare lo spazio massimo necessario, quindi ridurlo in seguito se trovi che è troppo. Ad esempio, l'aggiunta di due numeri da 100 "cifre" (dove la cifra è un int) non darà mai più di 101 cifre. Moltiplicare un numero di 12 cifre per un numero di 3 cifre non genererà mai più di 15 cifre (aggiungi i conteggi delle cifre).

2 / Per una maggiore velocità, normalizza (riduci lo spazio di archiviazione richiesto) i numeri solo se assolutamente necessario: la mia libreria aveva questa chiamata separata in modo che l'utente possa decidere tra velocità e problemi di archiviazione.

3 / L'addizione di un numero positivo e negativo è una sottrazione e la sottrazione di un numero negativo equivale all'aggiunta dell'equivalente positivo. È possibile salvare un bel po 'di codice facendo in modo che i metodi di aggiunta e sottrazione si chiamino l'un l'altro dopo aver regolato i segni.

4 / Evita di sottrarre numeri grandi da quelli piccoli poiché invariabilmente finisci con numeri come:

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

Invece, sottrai 10 da 11, quindi negalo:

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

Ecco i commenti (trasformati in testo) da una delle librerie per cui ho dovuto farlo. Il codice stesso è, sfortunatamente, protetto da copyright, ma potresti essere in grado di raccogliere informazioni sufficienti per gestire le quattro operazioni di base. Assumiamo di seguito che -ae -brappresentino numeri negativi e ae bsiano zero o numeri positivi.

Per l' addizione , se i segni sono diversi, usa la sottrazione della negazione:

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

Per la sottrazione , se i segni sono diversi, utilizzare l'aggiunta della negazione:

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

Anche una gestione speciale per assicurarci di sottrarre numeri piccoli da grandi:

small - big becomes -(big - small)

La moltiplicazione utilizza la matematica di livello base come segue:

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

Il modo in cui si ottiene ciò comporta l'estrazione di ciascuna delle cifre di 32 una alla volta (all'indietro), quindi l'utilizzo di add per calcolare un valore da aggiungere al risultato (inizialmente zero).

ShiftLefte le ShiftRightoperazioni vengono utilizzate per moltiplicare o dividere rapidamente a LongIntper il valore di avvolgimento (10 per la matematica "reale"). Nell'esempio sopra, aggiungiamo 475 a zero 2 volte (l'ultima cifra di 32) per ottenere 950 (risultato = 0 + 950 = 950).

Quindi abbiamo spostato a sinistra 475 per ottenere 4750 e lo shift a destra 32 per ottenere 3. Aggiungere 4750 a zero 3 volte per ottenere 14250, quindi aggiungere al risultato di 950 per ottenere 15200.

Spostamento a sinistra 4750 per ottenere 47500, spostamento a destra 3 per ottenere 0. Poiché lo spostamento a destra di 32 ora è zero, abbiamo finito e, in effetti, 475 x 32 è uguale a 15200.

Anche la divisione è complicata, ma basata sull'aritmetica iniziale (il metodo "gazinta" per "entra in"). Considera la seguente divisione lunga per 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.

Quindi 12345 / 27è 457con resto 6. Verificare:

  457 x 27 + 6
= 12339    + 6
= 12345

Ciò viene implementato utilizzando una variabile di abbassamento (inizialmente zero) per ridurre i segmenti di 12345 uno alla volta fino a quando non è maggiore o uguale a 27.

Quindi sottraiamo semplicemente 27 da quello fino a quando non scendiamo al di sotto di 27: il numero di sottrazioni è il segmento aggiunto alla riga superiore.

Quando non ci sono più segmenti da abbattere, abbiamo il nostro risultato.


Tieni presente che questi sono algoritmi piuttosto semplici. Ci sono modi molto migliori per eseguire operazioni aritmetiche complesse se i tuoi numeri saranno particolarmente grandi. Puoi esaminare qualcosa come GNU Multiple Precision Arithmetic Library : è sostanzialmente migliore e più veloce delle mie librerie.

Ha la sfortunata funzionalità sbagliata in quanto uscirà semplicemente se si esaurisce la memoria (un difetto piuttosto fatale per una libreria di uso generale secondo me) ma, se riesci a guardare oltre, è abbastanza bravo in quello che fa.

Se non puoi usarlo per motivi di licenza (o perché non vuoi che la tua applicazione esca semplicemente senza un motivo apparente), potresti almeno ottenere gli algoritmi da lì per l'integrazione nel tuo codice.

Ho anche scoperto che i corpi di MPIR (un fork di GMP) sono più suscettibili alle discussioni su potenziali cambiamenti - sembrano un gruppo più amichevole per gli sviluppatori.


14
Penso che tu abbia risposto "Voglio solo sapere se qualcuno ha o può fornire una spiegazione molto dettagliata e stupida dell'aritmetica a precisione arbitraria" MOLTO bene
Grant Peters

Una domanda successiva: è possibile impostare / rilevare carry e overflow senza accesso al codice macchina?
SasQ

8

Sebbene reinventare la ruota sia estremamente utile per l'edificazione e l'apprendimento personale, è anche un compito estremamente grande. Non voglio dissuadervi poiché è un esercizio importante e che ho fatto io stesso, ma dovreste essere consapevoli che ci sono problemi sottili e complessi al lavoro che i pacchetti più grandi affrontano.

Ad esempio, moltiplicazione. Ingenuamente, potresti pensare al metodo "scolaro", cioè scrivere un numero sopra l'altro, quindi fare lunghe moltiplicazioni come hai imparato a scuola. esempio:

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

ma questo metodo è estremamente lento (O (n ^ 2), n è il numero di cifre). Invece, i moderni pacchetti bignum usano una trasformata di Fourier discreta o una trasformata numerica per trasformarla in un'operazione essenzialmente O (n ln (n)).

E questo è solo per i numeri interi. Quando si entra in funzioni più complicate su qualche tipo di rappresentazione reale del numero (log, sqrt, exp, ecc.) Le cose si fanno ancora più complicate.

Se desideri un background teorico, consiglio vivamente di leggere il primo capitolo del libro di Yap, "Problemi fondamentali dell'algebra algoritmica" . Come già accennato, la libreria gmp bignum è un'ottima libreria. Per i numeri reali, ho usato mpfr e mi è piaciuto.


1
Mi interessa la parte su "usa una trasformata di Fourier discreta o una trasformata numerica per trasformarla in un'operazione essenzialmente O (n ln (n))" - come funziona? Solo un riferimento andrebbe bene :)
detly

1
@detly: la moltiplicazione polinomiale è la stessa della convoluzione, dovrebbe essere facile trovare informazioni sull'uso della FFT per eseguire una convoluzione veloce. Qualsiasi sistema numerico è un polinomio, dove le cifre sono coefficienti e la base è la base. Ovviamente dovrai prenderti cura dei carry per evitare di superare l'intervallo di cifre.
Ben Voigt

6

Non reinventare la ruota: potrebbe rivelarsi quadrata!

Usa una libreria di terze parti, come GNU MP , provata e testata.


4
Se vuoi imparare il C, metterei gli obiettivi un po 'più in basso. L'implementazione di una libreria bignum non è banale per tutti i tipi di motivi sottili che faranno inciampare uno studente
Mitch Wheat

3
Libreria di terze parti: d'accordo, ma GMP ha problemi di licenza (LGPL, sebbene effettivamente agisca come GPL poiché è un po 'difficile eseguire calcoli ad alte prestazioni attraverso un'interfaccia compatibile con LGPL).
Jason S

Bel riferimento Futurama (intenzionale?)
Grant Peters

7
GNU MP invoca incondizionatamente abort()errori di allocazione, che sono destinati a verificarsi con determinati calcoli follemente grandi. Questo è un comportamento inaccettabile per una libreria e una ragione sufficiente per scrivere il proprio codice di precisione arbitraria.
R .. GitHub SMETTA DI AIUTARE ICE

Devo essere d'accordo con R. Una libreria di uso generale che tira fuori semplicemente il tappeto da sotto il tuo programma quando la memoria si esaurisce è imperdonabile. Avrei preferito che sacrificassero un po 'di velocità per la sicurezza / recuperabilità.
paxdiablo

4

Lo fai praticamente nello stesso modo in cui lo fai con carta e matita ...

  • Il numero deve essere rappresentato in un buffer (array) in grado di assumere una dimensione arbitraria (che significa usare malloce realloc) secondo necessità
  • si implementa l'aritmetica di base il più possibile utilizzando strutture supportate dal linguaggio e si occupa di carry e spostamento manuale del punto-radice
  • setacciate i testi di analisi numerica per trovare argomenti efficienti per trattare con funzioni più complesse
  • si implementa solo quanto necessario.

In genere utilizzerai come unità di calcolo di base

  • byte contenenti 0-99 o 0-255
  • Parole a 16 bit contenenti appassimento 0-9999 o 0--65536
  • Parole a 32 bit contenenti ...
  • ...

come dettato dalla tua architettura.

La scelta della base binaria o decimale dipende dai tuoi desideri per la massima efficienza dello spazio, la leggibilità umana e la presenza dell'assenza di supporto matematico BCD (Binary Coded Decimal) sul tuo chip.


3

Puoi farlo con il livello di matematica delle scuole superiori. Sebbene nella realtà vengano utilizzati algoritmi più avanzati. Quindi, ad esempio, per aggiungere due numeri da 1024 byte:

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;
}

il risultato dovrà essere maggiore one placein caso di aggiunta per tenere conto dei valori massimi. Guarda questo :

9
   +
9
----
18

TTMath è un'ottima libreria se vuoi imparare. È costruito utilizzando C ++. L'esempio sopra era sciocco, ma è così che vengono fatte addizioni e sottrazioni in generale!

Un buon riferimento sull'argomento è la complessità computazionale delle operazioni matematiche . Ti dice quanto spazio è necessario per ogni operazione che desideri implementare. Ad esempio, se hai due N-digitnumeri, devi 2N digitsmemorizzare il risultato della moltiplicazione.

Come ha detto Mitch , non è di gran lunga un compito facile da implementare! Ti consiglio di dare un'occhiata a TTMath se conosci C ++.


Mi è venuto in mente l'uso degli array, ma sto cercando qualcosa di ancora più generale. Grazie per la risposta!
TT.

2
Hmm ... il nome del richiedente e il nome della biblioteca non possono essere una coincidenza, vero? ;)
John Y

LoL, non me ne sono accorto! Vorrei davvero che TTMath fosse mio :) A proposito, ecco una delle mie domande sull'argomento:
AraK


3

Uno dei riferimenti definitivi (IMHO) è TAOCP Volume II di Knuth. Spiega molti algoritmi per rappresentare numeri e operazioni aritmetiche su queste rappresentazioni.

@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

Supponendo che tu voglia scrivere un grande codice intero da solo, questo può essere sorprendentemente semplice da fare, parlato come qualcuno che lo ha fatto di recente (anche se in MATLAB.) Ecco alcuni dei trucchi che ho usato:

  • Ho memorizzato ogni singola cifra decimale come doppio numero. Ciò semplifica molte operazioni, soprattutto l'output. Anche se occupa più spazio di quanto si potrebbe desiderare, la memoria qui è economica e rende la moltiplicazione molto efficiente se è possibile convolgere una coppia di vettori in modo efficiente. In alternativa, puoi memorizzare diverse cifre decimali in un doppio, ma fai attenzione quindi che la convoluzione per fare la moltiplicazione può causare problemi numerici su numeri molto grandi.

  • Conserva un bit di segno separatamente.

  • L'addizione di due numeri è principalmente una questione di aggiunta delle cifre, quindi verifica la presenza di un riporto ad ogni passaggio.

  • La moltiplicazione di una coppia di numeri viene eseguita al meglio come convoluzione seguita da un passaggio di riporto, almeno se si dispone di un codice di convoluzione veloce alla spina.

  • Anche quando si memorizzano i numeri come una stringa di singole cifre decimali, è possibile eseguire la divisione (anche operazioni mod / rem) per ottenere circa 13 cifre decimali alla volta nel risultato. Questo è molto più efficiente di una divisione che funziona solo su 1 cifra decimale alla volta.

  • Per calcolare una potenza intera di un numero intero, calcola la rappresentazione binaria dell'esponente. Quindi utilizzare ripetute operazioni di squadratura per calcolare le potenze secondo necessità.

  • Molte operazioni (factoring, test di primalità, ecc.) Trarranno vantaggio da un'operazione powermod. Cioè, quando calcoli mod (a ^ p, N), riduci il risultato mod N ad ogni passo dell'elevamento a potenza in cui p è stato espresso in una forma binaria. Non calcolare prima a ^ p, quindi provare a ridurlo mod N.


1
Se stai memorizzando singole cifre piuttosto che in base 10 ^ 9 o in base 2 ^ 32 o qualcosa di simile, tutta la tua fantasia di convoluzione per moltiplicazione è semplicemente uno spreco. Big-O è abbastanza insignificante quando la tua costante è così cattiva ...
R .. GitHub STOP HELPING ICE

0

Ecco un semplice (ingenuo) esempio che ho fatto in PHP.

Ho implementato "Aggiungi" e "Moltiplica" e l'ho usato per un esempio di esponente.

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

Code 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);
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.