Saltar al contenido
Topos Uranos

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 n\sqrt{n} 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

  1. Implementar la prueba de primalidad por divisiones hasta n\sqrt{n} y justificar por qué basta llegar a n\sqrt{n}.
  2. Implementar el algoritmo de Euclides, enunciar su invariante (el máximo común divisor no cambia) y compararlo con std::gcd.
  3. Convertir un número a otra base por divisiones sucesivas y evaluar una representación posicional, o un polinomio, con la regla de Horner.
  4. 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 n≥2n \geq 2 es primo si sus únicos divisores positivos son 1 y nn; 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 dd entre 2 y n−1n - 1 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.

ProposiciónBasta probar hasta n\sqrt{n}

Si n≥2n \geq 2 es compuesto, tiene un divisor dd con 2≤d≤n2 \leq d \leq \sqrt{n}.

Demostración

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

    Si nn es compuesto, se escribe como producto de dos factores, ninguno igual a 1 ni a nn. Se nombra aa al menor de los dos.

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

    Multiplicando a≤ba \leq b por el positivo aa se obtiene a2≤ab=na^2 \leq ab = n, de donde a≤na \leq \sqrt{n}: el menor factor de toda factorización está a la izquierda de n\sqrt{n}.

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

    Los divisores se presentan, por tanto, en pares (d,n/d)(d, n/d) reflejados alrededor de n\sqrt{n}: con n=36n = 36, los pares son (2,18)(2, 18), (3,12)(3, 12), (4,9)(4, 9) y (6,6)(6, 6). Si ningún candidato hasta n\sqrt{n} divide a nn, 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 nn a unas n\sqrt{n}: para nn 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.

Programa en 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("{} es primo", n);    } else {        std::println("{} no es primo", n);    }}
Entrada del programa
2147483647
Salida
2147483647 es primo

Tres 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 n=1n = 1 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 d2≤nd^2 \leq n (la división entera trunca, pero dd no supera al cociente truncado exactamente cuando no supera a n/dn/d, porque dd es entero). El número de la entrada, 231−12^{31} - 1, es el mayor int y es primo; el programa lo decide con 46 33946\,339 divisiones.

El invariante del bucle es: ningún entero de 2 a d−1d - 1 divide a nn, mientras la bandera siga verdadera. Vale al entrar, cuando el intervalo de 2 a 1 está vacío; se conserva, porque si dd no divide a nn el intervalo crece en un número que no divide; y al salir con la bandera verdadera, el invariante y la condición falsa, d2>nd^2 > n, dicen juntos que ningún candidato hasta n\sqrt{n} divide a nn, que es lo que la proposición exige. La variante es la cantidad de candidatos que faltan por probar, es decir, de enteros entre dd y n\sqrt{n}: 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 d=46 341d = 46\,341, y entonces el producto 46 3412=2 147 488 28146\,341^2 = 2\,147\,488\,281 supera el máximo de int.

Programa en 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 del programa
2147483647
Salida
main.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 nn, 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 d=nd = n, que divide a nn, 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 nn por cada divisor hallado y siguiendo con el cociente.