Saltar para o conteúdo
Topos Uranos

Resumo

As aulas anteriores da unidade ensinaram a escrever laços e a raciocinar sobre eles com um invariante e uma variante; esta aula aplica esse raciocínio a quatro algoritmos numéricos clássicos, mais antigos que os computadores e ainda em uso. O teste de primalidade por divisões por tentativa, que para em n\sqrt{n} por uma razão que se demonstra, e cuja condição de parada estoura se for escrita com descuido. O algoritmo de Euclides para o máximo divisor comum, com seu invariante, sua terminação, sua armadilha com os negativos e sua comparação com std::gcd. A mudança de base por divisões sucessivas e a regra de Horner, que reconstrói um número a partir de seus algarismos e avalia um polinômio com o mesmo esquema. E a bissecção, que aproxima uma raiz partindo um intervalo em dois, garantida pelo teorema do valor intermediário, com um número de iterações que se calcula de antemão. Cada algoritmo é apresentado como um objeto matemático: com uma prova de que faz o que diz, uma prova de que termina e uma primeira contagem de seus passos.

Objetivos de aprendizagem

  1. Implementar o teste de primalidade por divisões até n\sqrt{n} e justificar por que basta chegar a n\sqrt{n}.
  2. Implementar o algoritmo de Euclides, enunciar seu invariante (o máximo divisor comum não muda) e compará-lo com std::gcd.
  3. Converter um número para outra base por divisões sucessivas e avaliar uma representação posicional, ou um polinômio, com a regra de Horner.
  4. Aproximar uma raiz com a bissecção até uma tolerância, e calcular quantas iterações ela exige.

Divisores e primos

Divisões por tentativa

Um inteiro n≥2n \geq 2 é primo se seus únicos divisores positivos são 1 e nn; se tem outro, é composto; 0 e 1 não são nem uma coisa nem outra. A definição sugere de imediato um algoritmo: testar cada candidato dd entre 2 e n−1n - 1 e declarar o número composto assim que um deles o divida. É o laço do menor divisor da aula Laços, e está correto. Ora, para um primo de dez algarismos ele faz quase dez bilhões de divisões, e quase todas são inúteis, pela razão seguinte.

ProposiçãoBasta testar até n\sqrt{n}

Se n≥2n \geq 2 é composto, tem um divisor dd com 2≤d≤n2 \leq d \leq \sqrt{n}.

Demonstração

  1. n=a⋅b,2≤a≤bn = a \cdot b, \quad 2 \leq a \leq b

    Se nn é composto, escreve-se como produto de dois fatores, nenhum igual a 1 nem a nn. Chama-se aa o menor dos dois.

  2. a2≤a⋅b=na^2 \leq a \cdot b = n

    Multiplicando a≤ba \leq b pelo positivo aa, obtém-se a2≤ab=na^2 \leq ab = n, donde a≤na \leq \sqrt{n}: o menor fator de toda fatoração está à esquerda de n\sqrt{n}.

  3. d≤n  ⟺  nd≥nd \leq \sqrt{n} \iff \frac{n}{d} \geq \sqrt{n}

    Os divisores apresentam-se, portanto, em pares (d,n/d)(d, n/d) refletidos em torno de n\sqrt{n}: com n=36n = 36, os pares são (2,18)(2, 18), (3,12)(3, 12), (4,9)(4, 9) e (6,6)(6, 6). Se nenhum candidato até n\sqrt{n} divide nn, tampouco divide algum além dele, porque seu par teria aparecido antes.

Segue-se que o número de divisões cai de cerca de nn para cerca de n\sqrt{n}: para nn próximo de dois bilhões, de dois bilhões para quarenta e seis mil. O programa usa o padrão da flag da aula Projetar laços corretos: isPrime começa verdadeira e o laço a desliga se encontra um divisor.

Programa em C++
#include <iostream>#include <print>​int main(){    int n{};    std::cin >> n;    bool isPrime{n >= 2};    for (int d{2}; isPrime && d <= n / d; ++d) {        if (n % d == 0) {            isPrime = false;        }    }    if (isPrime) {        std::println("{} é primo", n);    } else {        std::println("{} não é primo", n);    }}
Entrada do programa
2147483647
Saída
2147483647 é primo

Três decisões merecem comentário. A flag é inicializada com n >= 2, de modo que 0, 1 e os negativos são declarados não primos sem entrar no laço: esquecer esse caso é o erro típico, porque com n=1n = 1 o laço não faz nenhuma iteração e o programa declararia primo o 1. A condição do for tem duas partes: a flag, que detém o laço no primeiro divisor, e o limite. E o limite está escrito d <= n / d, que, para inteiros positivos, equivale a d2≤nd^2 \leq n (a divisão inteira trunca, mas dd não supera o quociente truncado exatamente quando não supera n/dn/d, porque dd é inteiro). O número da entrada, 231−12^{31} - 1, é o maior int e é primo; o programa o decide com 46 33946\,339 restos, os dos candidatos de 2 a 46 34046\,340.

O invariante do laço é: nenhum inteiro de 2 a d−1d - 1 divide nn, enquanto a flag continuar verdadeira. Vale ao entrar, quando o intervalo de 2 a 1 está vazio; conserva-se, porque, se dd não divide nn, o intervalo cresce com um número que não o divide; e, ao sair com a flag verdadeira, o invariante e a condição falsa, d2>nd^2 > n, dizem juntos que nenhum candidato até n\sqrt{n} divide nn, que é o que a proposição exige. A variante é a quantidade de candidatos que faltam testar, isto é, de inteiros entre dd e n\sqrt{n}: decresce em um por iteração e não fica abaixo de zero.

A condição que estoura

A maneira natural de escrever o limite é d * d <= n, e é um erro. Com a entrada anterior, o laço chega a d=46 341d = 46\,341, e então o produto 46 3412=2 147 488 28146\,341^2 = 2\,147\,488\,281 supera o máximo de int.

Programa em C++
#include <iostream>#include <print>​int main(){    int n{};    std::cin >> n;    bool isPrime{n >= 2};    for (int d{2}; isPrime && d * d <= n; ++d) {        if (n % d == 0) {            isPrime = false;        }    }    std::println("Primo? {}", isPrime);}
Entrada do programa
2147483647
Saída
main.cpp:9:33: runtime error: signed integer overflow: 46341 * 46341 cannot be represented in type 'int'

O programa terminou com o código 1.

O detector de comportamento indefinido detém o programa no produto. Sem ele, o estouro com sinal é comportamento indefinido, e o padrão não diz o que acontece. O que se observa com o GCC no Linux, com e sem otimização, é instrutivo: o produto dá a volta e resulta negativo ou menor que nn, e como nenhum int supera o máximo, a condição d * d <= n nunca mais deixa de se cumprir; o laço continua testando candidatos durante cerca de dois bilhões de iterações, até chegar a d=nd = n, que divide nn, e o programa declara composto um número primo. Um erro de estouro não se manifesta como um estouro, mas como uma resposta falsa. A forma d <= n / d não pode estourar, porque divide em vez de multiplicar; a aula Bits e biblioteca matemática oferecia outra saída, calcular o limite uma vez com std::sqrt, mas então a exatidão do limite depende do arredondamento do ponto flutuante, e a divisão inteira não tem esse problema.

A aula “Números primos e o teorema fundamental da aritmética”, do curso Álgebra e Geometria I, demonstra que todo inteiro maior que 1 é produto de primos de uma única maneira; as divisões por tentativa são a maneira mais direta de encontrar esse produto, dividindo nn por cada divisor encontrado e continuando com o quociente.