Explicação aritmética de precisão arbitrária


92

Estou tentando aprender C e descobri a incapacidade de trabalhar com números REALMENTE grandes (ou seja, 100 dígitos, 1000 dígitos, etc.). Estou ciente de que existem bibliotecas para fazer isso, mas quero tentar implementá-lo sozinho.

Só quero saber se alguém tem ou pode fornecer uma explicação bastante detalhada e simplificada da aritmética de precisão arbitrária.

Respostas:


162

É tudo uma questão de armazenamento e algoritmos adequados para tratar os números como partes menores. Vamos supor que você tenha um compilador no qual um intpode ser apenas de 0 a 99 e você deseja lidar com números até 999999 (vamos nos preocupar apenas com números positivos aqui para mantê-lo simples).

Você faz isso dando a cada número três se intusando as mesmas regras que você (deveria ter) aprendido na escola primária para adição, subtração e outras operações básicas.

Em uma biblioteca de precisão arbitrária, não há limite fixo para o número de tipos básicos usados ​​para representar nossos números, apenas o que a memória pode conter.

Adição, por exemplo 123456 + 78::

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

Trabalhando a partir da extremidade menos significativa:

  • transporte inicial = 0.
  • 56 + 78 + 0 transporte = 134 = 34 com 1 transporte
  • 34 + 00 + 1 transporte = 35 = 35 com 0 transporte
  • 12 + 00 + 0 transporte = 12 = 12 com 0 transporte

Esta é, de fato, como a adição geralmente funciona no nível de bits dentro de sua CPU.

A subtração é semelhante (usando subtração do tipo de base e emprestar em vez de carregar), a multiplicação pode ser feita com adições repetidas (muito lentas) ou produtos cruzados (mais rápido) e a divisão é mais complicada, mas pode ser feita por deslocamento e subtração dos números envolvido (a longa divisão que você teria aprendido quando criança).

Na verdade, escrevi bibliotecas para fazer esse tipo de coisa usando as potências máximas de dez que podem caber em um inteiro quando ao quadrado (para evitar estouro ao multiplicar dois ints juntos, como um de 16 bits intsendo limitado de 0 a 99 a gerar 9.801 (<32.768) quando elevado ao quadrado, ou 32 bits intusando 0 a 9.999 para gerar 99.980.001 (<2.147.483.648)), o que facilitou muito os algoritmos.

Alguns truques a serem observados.

1 / Ao somar ou multiplicar números, pré-aloque o espaço máximo necessário e reduza depois se achar que é muito. Por exemplo, adicionar dois números de 100 "dígitos" (onde dígito é um int) nunca fornecerá mais de 101 dígitos. Multiplicar um número de 12 dígitos por um número de 3 dígitos nunca gerará mais de 15 dígitos (adicione as contagens de dígitos).

2 / Para aumentar a velocidade, normalize (reduza o armazenamento necessário para) os números apenas se for absolutamente necessário - minha biblioteca tinha isso como uma chamada separada para que o usuário pudesse decidir entre as questões de velocidade e armazenamento.

3 / A adição de um número positivo e um negativo é uma subtração, e subtrair um número negativo é o mesmo que adicionar o positivo equivalente. Você pode economizar bastante código fazendo com que os métodos add e subtract chamem uns aos outros após ajustar os sinais.

4 / Evite subtrair números grandes de pequenos, pois você invariavelmente acaba com números como:

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

Em vez disso, subtraia 10 de 11 e, em seguida, negue:

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

Aqui estão os comentários (transformados em texto) de uma das bibliotecas para as quais tive que fazer isso. O código em si é, infelizmente, protegido por direitos autorais, mas você pode conseguir obter informações suficientes para lidar com as quatro operações básicas. Assuma a seguir que -ae -brepresentam números negativos e ae bsão zero ou números positivos.

Para adição , se os sinais forem diferentes, use a subtração da negação:

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

Para subtração , se os sinais forem diferentes, use a adição da negação:

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

Também tratamento especial para garantir que estamos subtraindo números pequenos de grandes:

small - big becomes -(big - small)

A multiplicação usa matemática básica da seguinte maneira:

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

A maneira como isso é obtido envolve a extração de cada um dos dígitos de 32, um de cada vez (para trás), em seguida, usando add para calcular um valor a ser adicionado ao resultado (inicialmente zero).

ShiftLefte as ShiftRightoperações são usadas para multiplicar ou dividir rapidamente a LongIntpelo valor de agrupamento (10 para matemática "real"). No exemplo acima, adicionamos 475 a zero 2 vezes (o último dígito de 32) para obter 950 (resultado = 0 + 950 = 950).

Em seguida, deslocamos à esquerda 475 para obter 4750 e à direita 32 para obter 3. Adicione 4750 a zero 3 vezes para obter 14.250 e, em seguida, adicione ao resultado 950 para obter 15200.

Desloque para a esquerda 4750 para obter 47500, deslocamento para a direita 3 para obter 0. Como o deslocado 32 para a direita agora é zero, terminamos e, de fato, 475 x 32 é igual a 15200.

A divisão também é complicada, mas baseada na aritmética primitiva (o método "gazinta" para "entra em"). Considere a seguinte divisão longa 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.

Portanto, 12345 / 27é 457com resto 6. Verificar:

  457 x 27 + 6
= 12339    + 6
= 12345

Isso é implementado usando uma variável de redução (inicialmente zero) para reduzir os segmentos de 12345, um de cada vez, até que seja maior ou igual a 27.

Então, simplesmente subtraímos 27 disso até chegarmos abaixo de 27 - o número de subtrações é o segmento adicionado à linha superior.

Quando não houver mais segmentos para derrubar, temos nosso resultado.


Lembre-se de que esses são algoritmos bastante básicos. Existem maneiras muito melhores de fazer aritmética complexa se seus números forem particularmente grandes. Você pode olhar para algo como GNU Multiple Precision Arithmetic Library - é substancialmente melhor e mais rápido do que minhas próprias bibliotecas.

Ele tem a característica infeliz de que ele simplesmente será encerrado se ficar sem memória (uma falha fatal para uma biblioteca de uso geral, na minha opinião), mas, se você puder ignorar isso, é muito bom no que faz.

Se você não pode usá-lo por motivos de licenciamento (ou porque não deseja que seu aplicativo seja encerrado sem motivo aparente), você poderia pelo menos obter os algoritmos de lá para integração em seu próprio código.

Eu também descobri que os bods do MPIR (um fork do GMP) são mais receptivos a discussões sobre mudanças potenciais - eles parecem um grupo mais amigável ao desenvolvedor.


14
Eu acho que você cobriu "Eu só quero saber se alguém tem ou pode fornecer uma explicação muito detalhada e simplificada da aritmética de precisão arbitrária" MUITO bem
Grant Peters

Uma pergunta de acompanhamento: É possível definir / detectar carregamentos e transbordamentos sem acesso ao código de máquina?
SasQ de

8

Embora reinventar a roda seja extremamente bom para sua edificação e aprendizado pessoal, também é uma tarefa extremamente grande. Não quero dissuadi-lo, pois é um exercício importante e que eu mesmo fiz, mas você deve estar ciente de que há questões sutis e complexas no trabalho que os pacotes maiores tratam.

Por exemplo, multiplicação. Ingenuamente, você pode pensar no método do 'colegial', ou seja, escreva um número acima do outro e faça uma multiplicação longa como aprendeu na escola. exemplo:

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

mas este método é extremamente lento (O (n ^ 2), sendo n o número de dígitos). Em vez disso, os pacotes bignum modernos usam uma transformada discreta de Fourier ou uma transformação numérica para transformar isso em uma operação essencialmente O (n ln (n)).

E isso é apenas para números inteiros. Quando você entra em funções mais complicadas em algum tipo de representação real de número (log, sqrt, exp, etc.), as coisas ficam ainda mais complicadas.

Se você gostaria de alguma base teórica, eu recomendo fortemente a leitura do primeiro capítulo do livro de Yap, "Fundamental Problems of Algorithmic Algebra" . Como já mencionado, a biblioteca gmp bignum é uma excelente biblioteca. Para números reais, usei mpfr e gostei.


1
Estou interessado na parte sobre "usar uma transformação discreta de Fourier ou uma transformação numérica para transformar isso em uma operação essencialmente O (n ln (n))" - como isso funciona? Apenas uma referência seria ótimo :)
provavelmente

1
@detly: multiplicação polinomial é o mesmo que convolução, deve ser fácil encontrar informações sobre como usar a FFT para realizar uma convolução rápida. Qualquer sistema numérico é um polinomial, onde os dígitos são coeficientes e a base é a base. Claro, você precisará cuidar dos carregadores para evitar exceder a faixa de dígitos.
Ben Voigt

6

Não reinvente a roda: pode acabar ficando quadrada!

Use uma biblioteca de terceiros, como GNU MP , que foi experimentada e testada.


4
Se você quiser aprender C, eu diminuiria um pouco sua visão. Implementar uma biblioteca bignum não é trivial por todos os tipos de razões sutis que podem confundir o aluno
Mitch Wheat

3
Biblioteca de terceiros: concordado, mas GMP tem problemas de licenciamento (LGPL, embora efetivamente atue como GPL, já que é meio difícil fazer matemática de alto desempenho por meio de uma interface compatível com LGPL).
Jason S

Referência de Nice Futurama (intencional?)
Grant Peters

7
GNU MP invoca incondicionalmente abort()falhas de alocação, que estão fadadas a acontecer com certas computações absurdamente grandes. Este é um comportamento inaceitável para uma biblioteca e razão suficiente para escrever seu próprio código de precisão arbitrária.
R .. GitHub PARAR DE AJUDAR O ICE

Eu tenho que concordar com R aí. Uma biblioteca de propósito geral que simplesmente puxa o tapete de baixo do seu programa quando a memória está acabando é imperdoável. Eu preferia que eles sacrificassem alguma velocidade por segurança / recuperabilidade.
paxdiablo

4

Você faz isso basicamente da mesma maneira que faz com lápis e papel ...

  • O número deve ser representado em um buffer (array) capaz de assumir um tamanho arbitrário (o que significa usar malloce realloc) conforme necessário
  • você implementa aritmética básica tanto quanto possível usando estruturas suportadas por linguagem, e lida com transportes e movendo o ponto raiz manualmente
  • você vasculha textos de análise numérica para encontrar argumentos eficientes para lidar com funções mais complexas
  • você só implementa o que precisa.

Normalmente, você usará como unidade básica de computação

  • bytes contendo 0-99 ou 0-255
  • Palavras de 16 bits contendo murchar 0-9999 ou 0--65536
  • Palavras de 32 bits contendo ...
  • ...

conforme ditado por sua arquitetura.

A escolha da base binária ou decimal depende de seus desejos de máxima eficiência de espaço, legibilidade humana e da ausência de suporte matemático Binary Coded Decimal (BCD) em seu chip.


3

Você pode fazer isso com o nível de ensino médio de matemática. Embora algoritmos mais avançados sejam usados ​​na realidade. Por exemplo, para adicionar dois 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;
}

o resultado deverá ser maior one placeem caso de adição para atender os valores máximos. Veja isso :

9
   +
9
----
18

TTMath é uma ótima biblioteca se você quiser aprender. Ele é construído em C ++. O exemplo acima era bobo, mas é assim que a adição e a subtração são feitas em geral!

Uma boa referência sobre o assunto é Complexidade computacional de operações matemáticas . Ele informa quanto espaço é necessário para cada operação que você deseja implementar. Por exemplo, se você tiver dois N-digitnúmeros, precisará 2N digitsarmazenar o resultado da multiplicação.

Como disse Mitch , não é uma tarefa fácil de implementar! Eu recomendo que você dê uma olhada no TTMath se você souber C ++.


O uso de matrizes me ocorreu, mas estou procurando algo ainda mais geral. Obrigado pela resposta!
TT.

2
Hmm ... o nome do autor da pergunta e o nome da biblioteca não podem ser uma coincidência, podem? ;)
John Y

LoL, eu não percebi isso! Eu realmente queria que o TTMath fosse meu :) A propósito, aqui está uma das minhas perguntas sobre o assunto:
AraK


3

Uma das referências finais (IMHO) é o TAOCP Volume II de Knuth. Ele explica muitos algoritmos para representar números e operações aritméticas nessas representações.

@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

Supondo que você queira escrever um grande código inteiro sozinho, isso pode ser surpreendentemente simples de fazer, falado como alguém que fez isso recentemente (embora no MATLAB). Aqui estão alguns dos truques que usei:

  • Guardei cada dígito decimal individual como um número duplo. Isso torna muitas operações simples, especialmente a saída. Embora ocupe mais armazenamento do que você deseja, a memória é barata aqui e torna a multiplicação muito eficiente se você puder convolver um par de vetores com eficiência. Alternativamente, você pode armazenar vários dígitos decimais em um duplo, mas tome cuidado, pois a convolução para fazer a multiplicação pode causar problemas numéricos em números muito grandes.

  • Armazene um pedaço de sinal separadamente.

  • A adição de dois números é principalmente uma questão de somar os dígitos e, em seguida, verificar se há transporte em cada etapa.

  • A multiplicação de um par de números é melhor feita como convolução seguida por uma etapa de transporte, pelo menos se você tiver um código de convolução rápido disponível.

  • Mesmo quando você armazena os números como uma seqüência de dígitos decimais individuais, a divisão (também operações mod / rem) pode ser feita para obter aproximadamente 13 dígitos decimais por vez no resultado. Isso é muito mais eficiente do que uma divisão que funciona em apenas 1 dígito decimal de cada vez.

  • Para calcular a potência de um inteiro, calcule a representação binária do expoente. Em seguida, use operações de quadratura repetidas para calcular as potências conforme necessário.

  • Muitas operações (fatoração, testes de primalidade, etc.) se beneficiarão de uma operação powermod. Ou seja, quando você calcula mod (a ^ p, N), reduza o mod N de resultado em cada etapa da exponenciação onde p foi expresso em uma forma binária. Não calcule a ^ p primeiro e, em seguida, tente reduzi-lo mod N.


1
Se você estiver armazenando dígitos individuais em vez de base-10 ^ 9 ou base-2 ^ 32 ou algo semelhante, todas as suas fantasias de convolução para multiplicação são simplesmente um desperdício. Big-O não faz sentido quando sua constante é tão ruim ...
R .. GitHub PARE DE AJUDAR O ICE

0

Aqui está um exemplo simples (ingênuo) que fiz em PHP.

Implementei "Adicionar" e "Multiplicar" e usei isso para um exemplo de expoente.

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);
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.