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 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
- Implementar o teste de primalidade por divisões até e justificar por que basta chegar a .
- Implementar o algoritmo de Euclides, enunciar seu invariante (o máximo divisor comum não muda) e compará-lo com
std::gcd. - 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.
- 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 é primo se seus únicos divisores positivos são 1 e ; 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 entre 2 e 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.
Se é composto, tem um divisor com .
Demonstração
Se é composto, escreve-se como produto de dois fatores, nenhum igual a 1 nem a . Chama-se o menor dos dois.
Multiplicando pelo positivo , obtém-se , donde : o menor fator de toda fatoração está à esquerda de .
Os divisores apresentam-se, portanto, em pares refletidos em torno de : com , os pares são , , e . Se nenhum candidato até divide , 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 para cerca de : para 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.
#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); }}21474836472147483647 é primoTrê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 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 (a divisão inteira trunca, mas não supera o quociente truncado exatamente quando não supera , porque é inteiro). O número da entrada, , é o maior int e é primo; o programa o decide com restos, os dos candidatos de 2 a .
O invariante do laço é: nenhum inteiro de 2 a divide , enquanto a flag continuar verdadeira. Vale ao entrar, quando o intervalo de 2 a 1 está vazio; conserva-se, porque, se não divide , 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, , dizem juntos que nenhum candidato até divide , que é o que a proposição exige. A variante é a quantidade de candidatos que faltam testar, isto é, de inteiros entre e : 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 , e então o produto supera o máximo de int.
#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);}2147483647main.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 , 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 , que divide , 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 por cada divisor encontrado e continuando com o quociente.
Carregando o conteúdo…