Краткое содержание
Предшествующие уроки раздела научили писать циклы и рассуждать о них с помощью инварианта и варианта цикла; этот урок применяет то же рассуждение к четырём классическим численным алгоритмам, которые старше компьютеров и до сих пор используются. Тест на простоту перебором делителей, который останавливается на по доказываемой причине и чьё условие остановки переполняется, если записать его небрежно. Алгоритм Евклида для наибольшего общего делителя, с его инвариантом, его завершением, его ловушкой с отрицательными числами и его сравнением с std::gcd. Перевод в другую систему счисления последовательными делениями и схема Горнера, которая восстанавливает число по его цифрам и по той же схеме вычисляет значение многочлена. И метод бисекции, который приближает корень, деля отрезок пополам, гарантирован теоремой о промежуточном значении и имеет число итераций, вычисляемое заранее. Каждый алгоритм представлен как математический объект: с доказательством того, что он делает то, что обещает, доказательством того, что он завершается, и первым подсчётом его шагов.
Цели обучения
- Реализовывать тест на простоту делениями до и обосновывать, почему достаточно дойти до .
- Реализовывать алгоритм Евклида, формулировать его инвариант (наибольший общий делитель не меняется) и сравнивать его с
std::gcd. - Переводить число в другую систему счисления последовательными делениями и вычислять значение позиционной записи, или многочлена, по схеме Горнера.
- Приближать корень методом бисекции с заданным допуском и вычислять, сколько итераций для этого нужно.
Делители и простые числа
Перебор делителей
Целое число называется простым, если его единственные положительные делители суть 1 и ; если у него есть другой делитель, оно составное; 0 и 1 не относятся ни к тем, ни к другим. Определение сразу подсказывает алгоритм: пробовать каждого кандидата от 2 до и объявлять число составным, как только один из них его разделит. Это цикл наименьшего делителя из урока Циклы, и он правилен. Однако для простого числа из десяти цифр он выполняет почти десять миллиардов делений, и почти все они бесполезны, по следующей причине.
Если составное, у него есть делитель , для которого .
Доказательство
Если составное, оно записывается как произведение двух множителей, ни один из которых не равен ни 1, ни . Обозначим через меньший из двух.
Умножая на положительное , получаем , откуда : меньший множитель любого разложения лежит слева от .
Итак, делители встречаются парами , симметричными относительно : при пары суть , , и . Если ни один кандидат до не делит , то и ни один дальше не делит, потому что его пара появилась бы раньше.
Отсюда следует, что число делений уменьшается примерно с примерно до : для порядка двух миллиардов это с двух миллиардов до сорока шести тысяч. Программа использует шаблон флага из урока Проектирование правильных циклов: isPrime сначала истинна, и цикл сбрасывает её, если находит делитель.
#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("Число {} простое", n); } else { std::println("Число {} не простое", n); }}2147483647Число 2147483647 простоеТри решения заслуживают комментария. Флаг инициализируется выражением n >= 2, так что 0, 1 и отрицательные числа объявляются непростыми без входа в цикл: забыть этот случай есть типичная ошибка, потому что при цикл не делает ни одной итерации и программа объявила бы 1 простым. Условие for состоит из двух частей: флага, который останавливает цикл на первом делителе, и границы. А граница записана как d <= n / d, что для положительных целых равносильно (целочисленное деление отбрасывает дробную часть, но не превосходит усечённого частного ровно тогда, когда не превосходит , потому что целое). Число из входных данных, , есть наибольшее значение int, и оно простое; программа устанавливает это за остатков, остатков от деления на кандидатов от 2 до .
Инвариант цикла таков: ни одно целое от 2 до не делит , пока флаг остаётся истинным. Он верен при входе, когда промежуток от 2 до 1 пуст; он сохраняется, потому что если не делит , то промежуток пополняется числом, которое не делит; а при выходе с истинным флагом инвариант и ложное условие, , вместе говорят, что ни один кандидат до не делит , а это и требует предложение. Вариант цикла есть количество кандидатов, которые осталось перебрать, то есть целых чисел между и : оно уменьшается на единицу за итерацию и не опускается ниже нуля.
Условие, которое переполняется
Естественный способ записать границу есть d * d <= n, и это ошибка. С теми же входными данными цикл доходит до , и тогда произведение превосходит максимум 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("Простое? {}", isPrime);}2147483647main.cpp:9:33: runtime error: signed integer overflow: 46341 * 46341 cannot be represented in type 'int'Программа завершилась с кодом 1.
Санитайзер неопределённого поведения останавливает программу на произведении. Без него знаковое переполнение есть неопределённое поведение, и стандарт не говорит, что происходит. То, что наблюдается с GCC в Linux, с оптимизацией и без неё, поучительно: произведение «заворачивается» и оказывается отрицательным или меньшим , а поскольку ни одно значение int не превосходит максимума, условие d * d <= n больше никогда не становится ложным; цикл продолжает перебирать кандидатов около двух миллиардов итераций, пока не дойдёт до , которое делит , и программа объявляет составным простое число. Ошибка переполнения проявляется не как переполнение, а как неверный ответ. Форма d <= n / d переполниться не может, потому что делит вместо того, чтобы умножать; урок Биты и математическая библиотека предлагал другой выход, вычислить границу один раз с помощью std::sqrt, но тогда точность границы зависит от округления в арифметике с плавающей точкой, а у целочисленного деления этой проблемы нет.
Урок «Простые числа и основная теорема арифметики» курса «Алгебра и геометрия I» доказывает, что каждое целое число, большее 1, единственным образом раскладывается в произведение простых; перебор делителей есть самый прямой способ найти это разложение: делить на каждый найденный делитель и продолжать с частным.
Загрузка содержимого…