Explication arithmétique de précision arbitraire


92

J'essaie d'apprendre C et j'ai rencontré l'incapacité de travailler avec VRAIMENT gros nombres (c'est-à-dire 100 chiffres, 1000 chiffres, etc.). Je suis conscient qu'il existe des bibliothèques pour ce faire, mais je veux essayer de l'implémenter moi-même.

Je veux juste savoir si quelqu'un a ou peut fournir une explication très détaillée et simplifiée de l'arithmétique à précision arbitraire.

Réponses:


162

Tout est une question de stockage adéquat et d'algorithmes pour traiter les nombres comme des parties plus petites. Supposons que vous ayez un compilateur dans lequel an intne peut être que de 0 à 99 et que vous souhaitiez gérer des nombres jusqu'à 999999 (nous ne nous soucierons que des nombres positifs ici pour rester simple).

Vous faites cela en donnant à chaque chiffre trois ints et en utilisant les mêmes règles que vous (auriez dû) apprendre à l'école primaire pour l'addition, la soustraction et les autres opérations de base.

Dans une bibliothèque de précision arbitraire, il n'y a pas de limite fixe sur le nombre de types de base utilisés pour représenter nos nombres, juste ce que la mémoire peut contenir.

Ajout par exemple 123456 + 78::

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

Travailler à partir de l'extrémité la moins significative:

  • report initial = 0.
  • 56 + 78 + 0 report = 134 = 34 avec 1 report
  • 34 + 00 + 1 report = 35 = 35 avec 0 report
  • 12 + 00 + 0 report = 12 = 12 avec 0 report

C'est en fait ainsi que l'addition fonctionne généralement au niveau du bit à l'intérieur de votre CPU.

La soustraction est similaire (en utilisant la soustraction du type de base et emprunter au lieu du report), la multiplication peut être effectuée avec des additions répétées (très lentes) ou des produits croisés (plus rapide) et la division est plus délicate mais peut être effectuée en décalant et en soustrayant les nombres impliqué (la longue division que vous auriez appris en tant qu'enfant).

J'ai en fait écrit des bibliothèques pour faire ce genre de choses en utilisant les puissances maximales de dix qui peuvent être insérées dans un entier au carré (pour éviter le débordement lors de la multiplication de deux ints ensemble, comme un 16 bits intlimité à 0 à 99 pour générer 9 801 (<32 768) au carré, ou 32 bits en intutilisant 0 à 9 999 pour générer 99 980 001 (<2 147 483 648)), ce qui a grandement facilité les algorithmes.

Quelques astuces à surveiller.

1 / Lors de l'ajout ou de la multiplication des nombres, pré-allouez l'espace maximum nécessaire puis réduisez plus tard si vous trouvez que c'est trop. Par exemple, l'ajout de deux nombres à 100 "chiffres" (où chiffre est un int) ne vous donnera jamais plus de 101 chiffres. Multiplier un nombre à 12 chiffres par un nombre à 3 chiffres ne générera jamais plus de 15 chiffres (ajoutez le nombre de chiffres).

2 / Pour plus de vitesse, ne normalisez (réduisez le stockage requis pour) les numéros que si c'est absolument nécessaire - ma bibliothèque avait cela comme un appel séparé afin que l'utilisateur puisse choisir entre la vitesse et les problèmes de stockage.

3 / L'addition d'un nombre positif et négatif est une soustraction, et soustraire un nombre négatif revient à ajouter l'équivalent positif. Vous pouvez économiser un peu de code en demandant aux méthodes d'ajout et de soustraction de s'appeler après avoir ajusté les signes.

4 / Évitez de soustraire les grands nombres aux petits car vous vous retrouvez invariablement avec des nombres comme:

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

Au lieu de cela, soustrayez 10 de 11, puis annulez-le:

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

Voici les commentaires (transformés en texte) de l'une des bibliothèques pour lesquelles j'ai dû faire cela. Le code lui-même est, malheureusement, protégé par copyright, mais vous pourrez peut-être choisir suffisamment d'informations pour gérer les quatre opérations de base. Supposons dans ce qui suit que -aet -breprésentent des nombres négatifs et aet bsont des nombres nuls ou positifs.

Pour l' addition , si les signes sont différents, utilisez la soustraction de la négation:

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

Pour la soustraction , si les signes sont différents, utilisez l'addition de la négation:

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

Également un traitement spécial pour nous assurer que nous soustrayons les petits nombres des grands:

small - big becomes -(big - small)

La multiplication utilise les mathématiques d'entrée de gamme comme suit:

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

La manière dont cela est réalisé consiste à extraire chacun des chiffres de 32 un à la fois (vers l'arrière) puis à utiliser add pour calculer une valeur à ajouter au résultat (initialement zéro).

ShiftLeftet les ShiftRightopérations sont utilisées pour multiplier ou diviser rapidement a LongIntpar la valeur d'enroulement (10 pour les mathématiques «réelles»). Dans l'exemple ci-dessus, nous ajoutons 475 à zéro 2 fois (le dernier chiffre de 32) pour obtenir 950 (résultat = 0 + 950 = 950).

Ensuite, nous avons laissé le décalage 475 pour obtenir 4750 et le décalage droit 32 pour obtenir 3. Ajouter 4750 à zéro 3 fois pour obtenir 14250 puis ajouter au résultat de 950 pour obtenir 15200.

Décalage gauche 4750 pour obtenir 47500, décalage droit 3 pour obtenir 0. Puisque le décalage 32 à droite est maintenant nul, nous avons terminé et, en fait, 475 x 32 équivaut à 15200.

La division est également délicate mais basée sur l'arithmétique précoce (la méthode «gazinta» pour «entre»). Considérez la longue division suivante pour 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.

Par conséquent 12345 / 27est 457avec le reste 6. Vérifier:

  457 x 27 + 6
= 12339    + 6
= 12345

Ceci est implémenté en utilisant une variable de tirage vers le bas (initialement zéro) pour réduire les segments de 12345 un par un jusqu'à ce qu'il soit supérieur ou égal à 27.

Ensuite, nous soustrayons simplement 27 de cela jusqu'à ce que nous soyons en dessous de 27 - le nombre de soustractions est le segment ajouté à la ligne supérieure.

Lorsqu'il n'y a plus de segments à abattre, nous avons notre résultat.


Gardez à l'esprit que ce sont des algorithmes assez basiques. Il existe de bien meilleures façons de faire de l'arithmétique complexe si vos nombres sont particulièrement importants. Vous pouvez consulter quelque chose comme la bibliothèque d'arithmétique de précision multiple GNU - c'est nettement meilleur et plus rapide que mes propres bibliothèques.

Il a l'inconvénient plutôt malheureux en ce sens qu'il se fermera simplement s'il manque de mémoire (un défaut plutôt fatal pour une bibliothèque à usage général à mon avis) mais, si vous pouvez regarder au-delà de cela, c'est assez bon dans ce qu'il fait.

Si vous ne pouvez pas l'utiliser pour des raisons de licence (ou parce que vous ne voulez pas que votre application se ferme sans raison apparente), vous pouvez au moins obtenir les algorithmes à partir de là pour les intégrer dans votre propre code.

J'ai également trouvé que les bods de MPIR (un fork de GMP) sont plus enclins à discuter des changements potentiels - ils semblent être plus conviviaux pour les développeurs.


14
Je pense que vous avez couvert "Je veux juste savoir si quelqu'un a ou peut fournir une explication très détaillée et simplifiée de l'arithmétique à précision arbitraire" TRÈS bien
Grant Peters

Une question de suivi: est-il possible de définir / détecter des portées et des débordements sans accès au code machine?
SasQ

8

Bien que réinventer la roue soit extrêmement bon pour votre édification et votre apprentissage personnels, c'est aussi une tâche extrêmement importante. Je ne veux pas vous en dissuader car c'est un exercice important et que j'ai fait moi-même, mais vous devez être conscient qu'il y a des problèmes subtils et complexes au travail que les paquets plus volumineux abordent.

Par exemple, la multiplication. Naïvement, vous pourriez penser à la méthode «écolier», c'est-à-dire écrire un nombre au-dessus de l'autre, puis faire de longues multiplications comme vous l'avez appris à l'école. exemple:

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

mais cette méthode est extrêmement lente (O (n ^ 2), n étant le nombre de chiffres). Au lieu de cela, les packages bignum modernes utilisent une transformation de Fourier discrète ou une transformation numérique pour transformer cela en une opération essentiellement O (n ln (n)).

Et ce n'est que pour les entiers. Quand vous entrez dans des fonctions plus compliquées sur un certain type de représentation réelle du nombre (log, sqrt, exp, etc.), les choses deviennent encore plus compliquées.

Si vous souhaitez un peu de fond théorique, je vous recommande vivement de lire le premier chapitre du livre de Yap, "Problèmes fondamentaux de l'algorithmique" . Comme déjà mentionné, la bibliothèque gmp bignum est une excellente bibliothèque. Pour les vrais chiffres, j'ai utilisé mpfr et je l'ai aimé.


1
Je m'intéresse à la partie sur "utiliser une transformée de Fourier discrète ou une transformation numérique pour transformer cela en une opération essentiellement O (n ln (n))" - comment cela fonctionne-t-il? Juste une référence serait bien :)
detly

1
@detly: la multiplication polynomiale est la même que la convolution, il devrait être facile de trouver des informations sur l'utilisation de la FFT pour effectuer une convolution rapide. Tout système numérique est un polynôme, où les chiffres sont des coefficients et la base est la base. Bien sûr, vous devrez vous occuper des portées pour éviter de dépasser la plage de chiffres.
Ben Voigt le

6

Ne réinventez pas la roue: elle peut s'avérer carrée!

Utilisez une bibliothèque tierce, telle que GNU MP , qui a fait ses preuves.


4
Si vous voulez apprendre C, je fixerais vos vues un peu plus bas. La mise en œuvre d'une bibliothèque bignum n'est pas triviale pour toutes sortes de raisons subtiles qui feront trébucher un apprenant
Mitch Wheat

3
Bibliothèque tierce: d'accord, mais GMP a des problèmes de licence (LGPL, bien qu'elle agisse effectivement comme GPL car il est un peu difficile de faire des calculs haute performance via une interface compatible LGPL).
Jason S

Référence Nice Futurama (intentionnelle?)
Grant Peters

7
GNU MP appelle inconditionnellement abort()des échecs d'allocation, qui sont inévitables avec certains calculs incroyablement volumineux. C'est un comportement inacceptable pour une bibliothèque et une raison suffisante pour écrire votre propre code de précision arbitraire.
R .. GitHub STOP HELPING ICE

Je dois être d'accord avec R là-bas. Une bibliothèque à usage général qui retire simplement le tapis de dessous votre programme lorsque la mémoire est faible est impardonnable. J'aurais préféré qu'ils sacrifient une certaine vitesse pour la sécurité / la récupérabilité.
paxdiablo

4

Vous le faites essentiellement de la même manière que vous le faites avec un crayon et du papier ...

  • Le nombre doit être représenté dans un tampon (tableau) capable de prendre une taille arbitraire (ce qui signifie utiliser mallocetrealloc ) selon les besoins
  • vous implémentez l'arithmétique de base autant que possible en utilisant des structures supportées par le langage, et vous traitez manuellement les portées et le déplacement du point de base
  • vous parcourez les textes d'analyse numérique pour trouver des arguments efficaces pour traiter des fonctions plus complexes
  • vous ne mettez en œuvre que ce dont vous avez besoin.

En règle générale, vous utiliserez comme unité de calcul de base

  • octets contenant 0-99 ou 0-255
  • Mots de 16 bits contenant le garrot 0-9999 ou 0-65536
  • Mots de 32 bits contenant ...
  • ...

comme dicté par votre architecture.

Le choix de la base binaire ou décimale dépend de vos désirs pour une efficacité spatiale maximale, une lisibilité humaine et la présence d'une absence de support mathématique Binary Coded Decimal (BCD) sur votre puce.


3

Vous pouvez le faire avec des mathématiques de niveau secondaire. Bien que des algorithmes plus avancés soient utilisés dans la réalité. Par exemple, pour ajouter deux nombres de 1024 octets:

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

le résultat devra être plus grand one placeen cas d'ajout pour tenir compte des valeurs maximales. Regarde ça :

9
   +
9
----
18

TTMath est une excellente bibliothèque si vous voulez apprendre. Il est construit en utilisant C ++. L'exemple ci-dessus était idiot, mais c'est ainsi que l'addition et la soustraction se font en général!

Une bonne référence sur le sujet est la complexité informatique des opérations mathématiques . Il vous indique l'espace requis pour chaque opération que vous souhaitez implémenter. Par exemple, si vous avez deux N-digitnombres, vous avez besoin2N digits stocker le résultat de la multiplication.

Comme l'a dit Mitch , ce n'est de loin pas une tâche facile à mettre en œuvre! Je vous recommande de jeter un oeil à TTMath si vous connaissez C ++.


L'utilisation de tableaux m'est venue à l'esprit, mais je cherche quelque chose d'encore plus général. Merci pour la réponse!
TT.

2
Hmm ... le nom du demandeur et le nom de la bibliothèque ne peuvent pas être une coïncidence, n'est-ce pas? ;)
John Y

LoL, je n'ai pas remarqué ça! Je souhaite vraiment que TTMath soit à moi :) Btw voici une de mes questions sur le sujet:
AraK


3

L'une des références ultimes (IMHO) est TAOCP Volume II de Knuth. Il explique de nombreux algorithmes pour représenter des nombres et des opérations arithmétiques sur ces représentations.

@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

En supposant que vous souhaitiez écrire vous-même un gros code entier, cela peut être étonnamment simple à faire, parlé comme quelqu'un qui l'a fait récemment (bien que dans MATLAB.) Voici quelques-unes des astuces que j'ai utilisées:

  • J'ai stocké chaque chiffre décimal individuel sous forme de nombre double. Cela simplifie de nombreuses opérations, en particulier la sortie. Bien que cela prenne plus de stockage que vous ne le souhaiteriez, la mémoire est bon marché ici et cela rend la multiplication très efficace si vous pouvez convoluer efficacement une paire de vecteurs. Alternativement, vous pouvez stocker plusieurs chiffres décimaux dans un double, mais attention alors que la convolution pour faire la multiplication peut poser des problèmes numériques sur de très grands nombres.

  • Stockez un bit de signe séparément.

  • L'ajout de deux nombres consiste principalement à ajouter les chiffres, puis à vérifier un report à chaque étape.

  • La multiplication d'une paire de nombres est mieux effectuée sous forme de convolution suivie d'une étape de report, du moins si vous avez un code de convolution rapide à portée de main.

  • Même lorsque vous stockez les nombres sous la forme d'une chaîne de chiffres décimaux individuels, la division (également les opérations mod / rem) peut être effectuée pour gagner environ 13 chiffres décimaux à la fois dans le résultat. C'est beaucoup plus efficace qu'une division qui ne fonctionne que sur 1 chiffre décimal à la fois.

  • Pour calculer une puissance entière d'un entier, calculez la représentation binaire de l'exposant. Ensuite, utilisez des opérations de quadrillage répétées pour calculer les puissances selon vos besoins.

  • De nombreuses opérations (factoring, tests de primalité, etc.) bénéficieront d'une opération powermod. Autrement dit, lorsque vous calculez mod (a ^ p, N), réduisez le résultat mod N à chaque étape de l'exponentiation où p a été exprimé sous forme binaire. Ne calculez pas d'abord a ^ p, puis essayez de le réduire mod N.


1
Si vous stockez des chiffres individuels plutôt que base-10 ^ 9 ou base-2 ^ 32 ou quelque chose de similaire, tous vos trucs de convolution pour multiplication sont tout simplement un gaspillage. Big-O n'a pas de sens quand votre constante est si mauvaise ...
R .. GitHub STOP HELPING ICE

0

Voici un exemple simple (naïf) que j'ai fait en PHP.

J'ai implémenté "Add" et "Multiply" et utilisé cela pour un exemple d'exposant.

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

Extrait de code

// 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);
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.