Resumen
Las clases anteriores de la unidad enseñaron a escribir bucles y a razonar sobre ellos con un invariante y una variante; esta clase aplica ese razonamiento a cuatro algoritmos numéricos clásicos, más antiguos que las computadoras y todavía en uso. La prueba de primalidad por divisiones de prueba, que se detiene en por una razón que se demuestra, y cuya condición de parada desborda si se escribe con descuido. El algoritmo de Euclides para el máximo común divisor, con su invariante, su terminación, su trampa con los negativos y su comparación con std::gcd. El cambio de base por divisiones sucesivas y la regla de Horner, que reconstruye un número a partir de sus cifras y evalúa un polinomio con el mismo esquema. Y la bisección, que aproxima una raíz partiendo un intervalo en dos, garantizada por el teorema de los valores intermedios, con un número de vueltas que se calcula de antemano. Cada algoritmo se presenta como un objeto matemático: con una prueba de que hace lo que dice, una prueba de que termina y un primer conteo de sus pasos.
Objetivos de aprendizaje
- Implementar la prueba de primalidad por divisiones hasta y justificar por qué basta llegar a .
- Implementar el algoritmo de Euclides, enunciar su invariante (el máximo común divisor no cambia) y compararlo con
std::gcd. - Convertir un número a otra base por divisiones sucesivas y evaluar una representación posicional, o un polinomio, con la regla de Horner.
- Aproximar una raíz con la bisección hasta una tolerancia, y calcular cuántas iteraciones exige.
Divisores y primos
Divisiones de prueba
Un entero es primo si sus únicos divisores positivos son 1 y ; si tiene otro, es compuesto; 0 y 1 no son ni lo uno ni lo otro. La definición sugiere de inmediato un algoritmo: probar cada candidato entre 2 y y declarar compuesto el número en cuanto uno divida. Es el bucle del menor divisor de la clase Bucles, y es correcto. Ahora bien, para un primo de diez cifras hace casi diez mil millones de divisiones, y casi todas son inútiles, por la razón siguiente.
Si es compuesto, tiene un divisor con .
Demostración
Si es compuesto, se escribe como producto de dos factores, ninguno igual a 1 ni a . Se nombra al menor de los dos.
Multiplicando por el positivo se obtiene , de donde : el menor factor de toda factorización está a la izquierda de .
Los divisores se presentan, por tanto, en pares reflejados alrededor de : con , los pares son , , y . Si ningún candidato hasta divide a , tampoco divide ninguno más allá, porque su pareja habría aparecido antes.
De ello se sigue que el número de divisiones baja de unas a unas : para cercano a dos mil millones, de dos mil millones a cuarenta y seis mil. El programa usa el patrón de la bandera de la clase Diseñar bucles correctos: isPrime empieza verdadera y el bucle la apaga si encuentra un 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("{} es primo", n); } else { std::println("{} no es primo", n); }}21474836472147483647 es primoTres decisiones merecen comentario. La bandera se inicializa con n >= 2, de modo que 0, 1 y los negativos se declaran no primos sin entrar al bucle: olvidar ese caso es el error típico, porque con el bucle no da ninguna vuelta y el programa declararía primo al 1. La condición del for tiene dos partes: la bandera, que detiene el bucle en el primer divisor, y el límite. Y el límite está escrito d <= n / d, que, para enteros positivos, equivale a (la división entera trunca, pero no supera al cociente truncado exactamente cuando no supera a , porque es entero). El número de la entrada, , es el mayor int y es primo; el programa lo decide con divisiones.
El invariante del bucle es: ningún entero de 2 a divide a , mientras la bandera siga verdadera. Vale al entrar, cuando el intervalo de 2 a 1 está vacío; se conserva, porque si no divide a el intervalo crece en un número que no divide; y al salir con la bandera verdadera, el invariante y la condición falsa, , dicen juntos que ningún candidato hasta divide a , que es lo que la proposición exige. La variante es la cantidad de candidatos que faltan por probar, es decir, de enteros entre y : decrece en uno por vuelta y no baja de cero.
La condición que desborda
La manera natural de escribir el límite es d * d <= n, y es un error. Con la entrada anterior, el bucle llega a , y entonces el producto supera el 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'El programa terminó con el código 1.
El detector de comportamiento indefinido detiene el programa en el producto. Sin él, el desbordamiento con signo es comportamiento indefinido, y la norma no dice qué ocurre. Lo que se observa con GCC en Linux, con y sin optimización, es instructivo: el producto da la vuelta y resulta negativo o menor que , y como ningún int supera al máximo, la condición d * d <= n ya no deja de cumplirse; el bucle sigue probando candidatos durante unos dos mil millones de vueltas, hasta llegar a , que divide a , y el programa declara compuesto a un número primo. Un error de desbordamiento no se manifiesta como un desbordamiento, sino como una respuesta falsa. La forma d <= n / d no puede desbordar, porque divide en lugar de multiplicar; la clase Bits y biblioteca matemática ofrecía otra salida, calcular el límite una vez con std::sqrt, pero entonces la exactitud del límite depende del redondeo de la coma flotante, y la división entera no tiene ese problema.
La clase «Números primos y el teorema fundamental de la aritmética», del curso Álgebra y Geometría I, demuestra que todo entero mayor que 1 es producto de primos de una sola manera; las divisiones de prueba son la manera más directa de encontrar ese producto, dividiendo por cada divisor hallado y siguiendo con el cociente.
Cargando el contenido…