Explicación aritmética de precisión arbitraria

92

Estoy tratando de aprender C y me he encontrado con la incapacidad de trabajar con números REALMENTE grandes (es decir, 100 dígitos, 1000 dígitos, etc.). Soy consciente de que existen bibliotecas para hacer esto, pero quiero intentar implementarlo yo mismo.

Solo quiero saber si alguien tiene o puede proporcionar una explicación muy detallada y simplista de la aritmética de precisión arbitraria.

TT.
fuente

Respuestas:

162

Todo es cuestión de almacenamiento y algoritmos adecuados para tratar los números como partes más pequeñas. Supongamos que tiene un compilador en el que an intsolo puede ser de 0 a 99 y desea manejar números hasta 999999 (aquí solo nos preocuparemos por los números positivos para que sea más simple).

Lo haces dando a cada número tres inty usando las mismas reglas que (deberías haber) aprendido en la escuela primaria para la suma, resta y las otras operaciones básicas.

En una biblioteca de precisión arbitraria, no hay un límite fijo en la cantidad de tipos de base utilizados para representar nuestros números, solo lo que la memoria pueda contener.

Adición por ejemplo 123456 + 78:

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

Trabajando desde el extremo menos significativo:

  • acarreo inicial = 0.
  • 56 + 78 + 0 acarreo = 134 = 34 con 1 acarreo
  • 34 + 00 + 1 acarreo = 35 = 35 con 0 acarreo
  • 12 + 00 + 0 acarreo = 12 = 12 con 0 acarreo

De hecho, así es como funciona generalmente la suma a nivel de bits dentro de su CPU.

La resta es similar (usando la resta del tipo base y pedir prestado en lugar de llevar), la multiplicación se puede hacer con sumas repetidas (muy lento) o productos cruzados (más rápido) y la división es más complicada, pero se puede hacer cambiando y restando los números. involucrados (la división larga que habrías aprendido de niño).

De hecho, he escrito bibliotecas para hacer este tipo de cosas usando las potencias máximas de diez que pueden encajar en un número entero cuando se eleva al cuadrado (para evitar el desbordamiento al multiplicar dos ints juntos, como un 16 bits intlimitado a 0 a 99 para generar 9.801 (<32.768) cuando se eleva al cuadrado, o 32 bits intutilizando 0 a 9.999 para generar 99.980.001 (<2.147.483.648)) lo que facilitó enormemente los algoritmos.

Algunos trucos a tener en cuenta.

1 / Al sumar o multiplicar números, pre-asigne el espacio máximo necesario y luego reduzca más tarde si encuentra que es demasiado. Por ejemplo, agregar dos números de 100 "dígitos" (donde el dígito es un int) nunca le dará más de 101 dígitos. Multiplicar un número de 12 dígitos por un número de 3 dígitos nunca generará más de 15 dígitos (sume los recuentos de dígitos).

2 / Para mayor velocidad, normalice (reduzca el almacenamiento requerido para) los números solo si es absolutamente necesario; mi biblioteca tenía esto como una llamada separada para que el usuario pueda decidir entre problemas de velocidad y almacenamiento.

3 / La suma de un número positivo y negativo es una resta, y restar un número negativo es lo mismo que sumar el equivalente positivo. Puede guardar bastante código haciendo que los métodos de suma y resta se llamen entre sí después de ajustar los signos.

4 / Evite restar números grandes de pequeños, ya que invariablemente terminará con números como:

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

En cambio, reste 10 de 11, luego néguelo:

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

Aquí están los comentarios (convertidos en texto) de una de las bibliotecas para las que tuve que hacer esto. Desafortunadamente, el código en sí está protegido por derechos de autor, pero es posible que pueda seleccionar suficiente información para manejar las cuatro operaciones básicas. Suponga en lo siguiente que -ay -brepresentan números negativos y ay bson cero o números positivos.

Para la suma , si los signos son diferentes, use la resta de la negación:

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

Para la resta , si los signos son diferentes, use la suma de la negación:

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

También un manejo especial para asegurarnos de que restamos números pequeños de grandes:

small - big becomes -(big - small)

La multiplicación utiliza matemáticas de nivel de entrada de la siguiente manera:

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 forma en que esto se logra implica extraer cada uno de los dígitos de 32 uno a la vez (hacia atrás) y luego usar sumar para calcular un valor que se agregará al resultado (inicialmente cero).

ShiftLefty las ShiftRightoperaciones se utilizan para multiplicar o dividir rápidamente a LongIntpor el valor de ajuste (10 para matemáticas "reales"). En el ejemplo anterior, sumamos 475 a cero 2 veces (el último dígito de 32) para obtener 950 (resultado = 0 + 950 = 950).

Luego, dejamos el cambio 475 para obtener 4750 y el de la derecha 32 para obtener 3. Suma 4750 a cero 3 veces para obtener 14250 y luego suma al resultado de 950 para obtener 15200.

Desplazamiento a la izquierda 4750 para obtener 47500, desplazamiento a la derecha 3 para obtener 0. Dado que el desplazamiento a la derecha 32 ahora es cero, hemos terminado y, de hecho, 475 x 32 es igual a 15200.

La división también es complicada pero se basa en la aritmética temprana (el método "gazinta" para "entra"). Considere la siguiente división larga para 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.

Por tanto 12345 / 27es 457con resto 6. Verificar:

  457 x 27 + 6
= 12339    + 6
= 12345

Esto se implementa mediante el uso de una variable de reducción (inicialmente cero) para reducir los segmentos de 12345 uno a la vez hasta que sea mayor o igual a 27.

Luego, simplemente restamos 27 de eso hasta que lleguemos por debajo de 27: el número de restas es el segmento agregado a la línea superior.

Cuando no hay más segmentos para derribar, tenemos nuestro resultado.


Tenga en cuenta que estos son algoritmos bastante básicos. Hay formas mucho mejores de hacer aritmética compleja si sus números van a ser particularmente grandes. Puede buscar algo como la biblioteca aritmética de precisión múltiple de GNU : es sustancialmente mejor y más rápido que mis propias bibliotecas.

Tiene el desafortunado error de que simplemente se cerrará si se queda sin memoria (una falla bastante fatal para una biblioteca de propósito general en mi opinión) pero, si puede mirar más allá de eso, es bastante bueno en lo que hace.

Si no puede usarlo por razones de licencia (o porque no desea que su aplicación simplemente salga sin razón aparente), al menos podría obtener los algoritmos de allí para integrarlos en su propio código.

También descubrí que los cuerpos de MPIR (una bifurcación de GMP) son más receptivos a las discusiones sobre cambios potenciales; parecen un grupo más amigable para los desarrolladores.

paxdiablo
fuente
14
Creo que cubriste "Solo quiero saber si alguien tiene o puede proporcionar una explicación muy detallada y simplista de la aritmética de precisión arbitraria" MUY bien
Grant Peters
Una pregunta de seguimiento: ¿Es posible configurar / detectar acarreos y desbordamientos sin acceso al código de la máquina?
SasQ
8

Si bien reinventar la rueda es extremadamente bueno para su edificación y aprendizaje personal, también es una tarea extremadamente grande. No quiero disuadirlo, ya que es un ejercicio importante y uno que yo mismo he hecho, pero debe tener en cuenta que hay problemas sutiles y complejos en el trabajo que los paquetes más grandes abordan.

Por ejemplo, multiplicación. Ingenuamente, podría pensar en el método del 'colegial', es decir, escribir un número encima del otro y luego hacer una multiplicación larga como aprendió en la escuela. ejemplo:

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

pero este método es extremadamente lento (O (n ^ 2), siendo n el número de dígitos). En cambio, los paquetes bignum modernos utilizan una transformada de Fourier discreta o una transformada numérica para convertir esto en una operación esencialmente O (n ln (n)).

Y esto es solo para números enteros. Cuando entra en funciones más complicadas en algún tipo de representación real de número (log, sqrt, exp, etc.), las cosas se vuelven aún más complicadas.

Si desea algo de base teórica, le recomiendo que lea el primer capítulo del libro de Yap, "Problemas fundamentales del álgebra algorítmica" . Como ya se mencionó, la biblioteca gmp bignum es una excelente biblioteca. Para números reales, he usado mpfr y me gustó.


fuente
1
Estoy interesado en la parte sobre "usar una transformada de Fourier discreta o una transformada numérica para convertir esto en una operación esencialmente O (n ln (n))". ¿Cómo funciona? Solo una referencia estaría bien :)
detly
1
@detly: la multiplicación de polinomios es lo mismo que la convolución, debería ser fácil encontrar información sobre el uso de la FFT para realizar una convolución rápida. Cualquier sistema numérico es un polinomio, donde los dígitos son coeficientes y la base es la base. Por supuesto, deberás cuidar los acarreos para evitar exceder el rango de dígitos.
Ben Voigt
6

No reinventes la rueda: ¡puede resultar cuadrada!

Utilice una biblioteca de terceros, como GNU MP , que esté probada y comprobada.

Trigo Mitch
fuente
4
Si quieres aprender C, pondría tus miras un poco más bajas. Implementar una biblioteca bignum no es trivial por todo tipo de razones sutiles que harán tropezar a un alumno
Mitch Wheat
3
Biblioteca de terceros: se acordó, pero GMP tiene problemas de licencia (LGPL, aunque efectivamente actúa como GPL ya que es un poco difícil hacer cálculos de alto rendimiento a través de una interfaz compatible con LGPL).
Jason S
Buena referencia de Futurama (¿intencional?)
Grant Peters
7
GNU MP invoca incondicionalmente abort()las fallas de asignación, que seguramente sucederán con ciertos cálculos increíblemente grandes. Este es un comportamiento inaceptable para una biblioteca y razón suficiente para escribir su propio código de precisión arbitraria.
R .. GitHub DEJA DE AYUDAR A ICE
Tengo que estar de acuerdo con R en eso. Una biblioteca de propósito general que simplemente saca la alfombra de debajo de su programa cuando la memoria se agota es imperdonable. Hubiera preferido que sacrificaran algo de velocidad por seguridad / recuperabilidad.
paxdiablo
4

Lo haces básicamente de la misma manera que lo haces con lápiz y papel ...

  • El número debe representarse en un búfer (matriz) capaz de tomar un tamaño arbitrario (lo que significa usar mallocy realloc) según sea necesario
  • implementa la aritmética básica tanto como sea posible utilizando estructuras compatibles con el lenguaje, y maneja los acarreos y mueve el punto de base manualmente
  • recorre textos de análisis numérico para encontrar argumentos eficientes para tratar con funciones más complejas
  • solo implementa lo que necesita.

Normalmente utilizará como unidad básica de cálculo

  • bytes que contienen 0-99 o 0-255
  • Palabras de 16 bits que contienen wither 0-9999 o 0-65536
  • Palabras de 32 bits que contienen ...
  • ...

según lo dictado por su arquitectura.

La elección de la base binaria o decimal depende de sus deseos de máxima eficiencia de espacio, legibilidad humana y la presencia de ausencia de soporte matemático decimal codificado en binario (BCD) en su chip.

dmckee --- ex-gatito moderador
fuente
3

Puedes hacerlo con un nivel de matemáticas de secundaria. Aunque en realidad se utilizan algoritmos más avanzados. Entonces, por ejemplo, para agregar dos números de 1024 bytes:

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

El resultado tendrá que ser mayor one placeen caso de adición para cuidar los valores máximos. Mira este :

9
   +
9
----
18

TTMath es una gran biblioteca si quieres aprender. Está construido con C ++. El ejemplo anterior fue tonto, ¡pero así es como se hace la suma y la resta en general!

Una buena referencia sobre el tema es la complejidad computacional de las operaciones matemáticas . Le dice cuánto espacio se requiere para cada operación que desea implementar. Por ejemplo, si tiene dos N-digitnúmeros, entonces necesita 2N digitsalmacenar el resultado de la multiplicación.

Como dijo Mitch , ¡no es una tarea fácil de implementar! Te recomiendo que eches un vistazo a TTMath si conoces C ++.

AraK
fuente
Se me ocurrió el uso de matrices, pero estoy buscando algo aún más general. ¡Gracias por la respuesta!
TT.
2
Hmm ... el nombre del autor de la pregunta y el nombre de la biblioteca no pueden ser una coincidencia, ¿verdad? ;)
John Y
¡LoL, no me di cuenta de eso! Ojalá realmente TTMath fuera mío :) Por cierto, aquí está una de mis preguntas sobre el tema:
AraK
3

Una de las últimas referencias (en mi humilde opinión) es el volumen II de TAOCP de Knuth. Explica muchos algoritmos para representar números y operaciones aritméticas en estas representaciones.

@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},
}
Marc van Dongen
fuente
1

Suponiendo que desea escribir un código entero grande usted mismo, esto puede ser sorprendentemente simple de hacer, hablado como alguien que lo hizo recientemente (aunque en MATLAB). Estos son algunos de los trucos que utilicé:

  • Guardé cada dígito decimal individual como un número doble. Esto simplifica muchas operaciones, especialmente la salida. Si bien ocupa más almacenamiento de lo que desearía, la memoria es barata aquí y hace que la multiplicación sea muy eficiente si puede convertir un par de vectores de manera eficiente. Alternativamente, puede almacenar varios dígitos decimales en un doble, pero tenga en cuenta que la convolución para hacer la multiplicación puede causar problemas numéricos en números muy grandes.

  • Almacene un bit de señal por separado.

  • La suma de dos números es principalmente una cuestión de sumar los dígitos y luego verificar si hay un acarreo en cada paso.

  • La multiplicación de un par de números se realiza mejor como convolución seguida de un paso de acarreo, al menos si tiene un código de convolución rápido disponible.

  • Incluso cuando almacena los números como una cadena de dígitos decimales individuales, se puede realizar la división (también las operaciones mod / rem) para obtener aproximadamente 13 dígitos decimales a la vez en el resultado. Esto es mucho más eficiente que una división que funciona con solo 1 dígito decimal a la vez.

  • Para calcular la potencia de un número entero, calcule la representación binaria del exponente. Luego use operaciones de cuadratura repetidas para calcular las potencias según sea necesario.

  • Muchas operaciones (factorización, pruebas de primalidad, etc.) se beneficiarán de una operación powermod. Es decir, cuando calcule mod (a ^ p, N), reduzca el resultado mod N en cada paso de la exponenciación donde p se ha expresado en forma binaria. No calcule a ^ p primero y luego intente reducirlo mod N.


fuente
1
Si está almacenando dígitos individuales en lugar de base-10 ^ 9 o base-2 ^ 32 o algo similar, todas sus cosas elegantes de convolución para multiplicación son simplemente un desperdicio. Big-O es prácticamente insignificante cuando su constante es que mal ...
R .. GitHub PARADA DE HIELO QUE AYUDA
0

Aquí hay un ejemplo simple (ingenuo) que hice en PHP.

Implementé "Sumar" y "Multiplicar" y lo usé como ejemplo de exponente.

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

Recorte de código

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