Привет, Хабр!
Операция проверки делимости — одна из фундаментальных в информатике и теории чисел. Для обычных чисел, помещающихся в машинное слово, это одна быстрая аппаратная инструкция. Но для очень больших целых чисел (Big Integers), размер которых превышает разрядность регистра процессора, классическое взятие остатка становится ресурсоёмкой многословной процедурой.
Эта статья предлагает чёткую и явную формулировку детерминированного алгоритма для проверки делимости целого числа на нечётный делитель
, родственного бинарному алгоритму Евклида. Его ключевая особенность: он использует исключительно операции сложения (
) и деления на 2 (побитового сдвига вправо,
), что позволяет полностью избежать дорогой операции взятия остатка в его явном виде.
Основная идея и практическая ценность
Традиционная проверка делимости сводится к вычислению остатка
. Данный подход итеративно преобразует число
в меньшее число
, сохраняя при этом инвариант делимости:
.
Основная практическая ценность заключается в эффективности для больших целых чисел (Big Integers) и аппаратно-ориентированных реализаций (FPGA/ASIC), поскольку сложение и сдвиг являются существенно более дешёвыми операциями, чем деление.
Сравнение сложности (для Big Integers)
Критически важно понимать: асимптотическая сложность данного алгоритма не является его главным преимуществом. Его сила — в низком константном множителе и аппаратной простоте.
Характеристика Традиционное деление / N % d Итерационный бинарный критерий
Ключевые операции Дорогостоящее многословное деление/умножение Дешёвые многословные сложение и сдвиг
Асимптотика для BigInt или
Тип преимущества – Крайне низкий константный множитель и кардинально меньшая сложность аппаратной реализации
В чём заключается реальное практическое преимущество?
Несмотря на схожую асимптотику с наивным делением в столбик (
), преимущество алгоритма кроется в типе используемых операций и их стоимости на уровне процессора или логических схем.
-
Константный фактор: Даже продвинутые алгоритмы деления (с квазилинейной сложностью
) имеют большой константный накладной расход из-за внутренней сложности. Наш алгоритм заменяет эту дорогую операцию на последовательность элементарных шагов (сложение+сдвиг), каждый из которых выполняется с линейной сложностью
и минимальными накладными расходами. Это даёт радикальное снижение константного множителя времени выполнения.
Аппаратная эффективность: На платформах типа FPGA/ASIC блоки сложения и сдвига требуют на порядки меньше логических элементов и энергии, чем полноценные блоки целочисленного деления. Это делает алгоритм идеальным кандидатом для встраивания в специализированные IP-ядра или криптографические ускорители.
Сравнение с аналогами и контекст
Связь с бинарным НОД: Алгоритм логически эквивалентен бинарному алгоритму Евклида для вычисления , так как условие
равносильно
. Однако данная формулировка явно нацелена на проверку делимости и использует только сложение (вместо вычитания), что в некоторых конвейерных аппаратных реализациях может быть проще.
Почему не везде используется? В высокооптимизированных библиотеках (GMP) для разовой проверки делимости прямое вычисление НОД часто оказывается менее эффективным, чем специализированные методы. Ценность данного подхода проявляется в нишевых сценариях: массовая проверка на один делитель, аппаратная реализация, образовательные цели.
Алгоритм: Шаги и Псевдокод
Алгоритм сначала сводит задачу к случаю, когда делитель — нечётный.
Предварительная обработка чётного делителя
Если чётно, оно представляется в виде:
Пусть обозначает показатель максимальной степени двойки, делящей
(количество младших нулей, CTZ).
Сначала проверяется, делится ли на
: если
то не делится на
.
Иначе, производится редукция:
N′←N≫k,d′←d≫k
Далее проверяется делимость на нечётное
.
2. Основная процедура для нечётного делителя
Вход: , нечётный
. Выход: True, если
; False иначе.
Инициализация:
X←N
Цикл:
Нормализация по 2: Делим на 2 (сдвиг вправо), пока оно не станет нечётным.
Проверка завершения:
Если , вернуть True.
Если , вернуть False.
Итерация: Если
, выполнить
и перейти к шагу 1.
Псевдокод: Итерационный бинарный критерий делимости
function DIVIDES_BY_D(N: integer ≥ 0, d: integer > 0) -> boolean:
if d == 1:
return True
// 1. Обработка чётного d
if (d & 1) == 0:
k = v2(d) // v2(x): count trailing zeros (CTZ)
if v2(N) < k:
return False
N = N >> k // Убираем множитель 2^k из N
d = d >> k // Теперь d гарантированно нечётно
// 2. Основная процедура для нечётного d
X = N
while True:
// 2.1. Нормализация: удаление всех факторов 2
r = v2(X)
if r > 0:
X = X >> r
// 2.2. Проверка условий останова
if X == d:
return True
if X < d:
return False
// 2.3. Шаг итерации (X <- X + d)
X = X + d
Теоретическое обоснование
Инвариант и корректность
Данный алгоритм основан на том же фундаментальном принципе, что и Бинарный алгоритм Евклида (Binary GCD): . Данный метод является его специализированной версией, адаптированной для быстрой проверки делимости (
).
На каждой итерации алгоритма сохраняется ключевой инвариант:
Поскольку нечётно,
. Операции
и
(деление на степень двойки) сохраняют инвариант.
Критический момент (Сохранение делимости при целочисленном делении):
В общем случае целочисленное деление не сохраняет конгруэнцию по произвольному модулю. Однако, поскольку делитель
всегда нечётен, он взаимно прост со степенью двойки
, то есть
.
Следовательно, согласно свойству делимости (теорема Гаусса), операция деления на
полностью сохраняет инвариант делимости, который является целью нашего алгоритма:
Это гарантирует, что мы можем безопасно использовать только побитовые сдвиги.
4. Анализ сложности
Количество итераций (циклов while) алгоритма оценивается как , что эквивалентно
(где
— длина числа
в битах).
Общая вычислительная сложность для многословных чисел (Big Integers):
Так как стоимость одной итерации (сложение или сдвиг
) для
машинных слов составляет
, а количество итераций равно
, общая сложность алгоритма составляет:
Алгоритм обладает квадратичной сложностью, как и классическое деление в столбик. Его практическая эффективность обусловлена низкой константой, скрытой за , поскольку используются только простейшие арифметические операции.
Конечность: В случае делимости (), после нормализации получаем
, где
— нечётная часть
. Шаг
даёт
. Поскольку
нечётно,
чётно. После нормализации нечётный множитель
строго уменьшается, гарантируя завершение. В случае неделимости процесс сойдётся к
, равному остатку
(
), и вернёт False.
Пример работы
Проверим делимость на
.
Шаг |
X (Нечётное на старте) |
Операция: X = X + d (Сложение) |
Нормализация: v₂ (Кол-во нулей) и Деление на 2ⁿ |
Результат X (Новое нечётное) |
|---|---|---|---|---|
0 |
3519 |
3519 + 9 = 3528 |
v₂(3528) = 3, Деление на 8 |
441 |
1 |
441 |
441 + 9 = 450 |
v₂(450) = 1, Деление на 2 |
225 |
2 |
225 |
225 + 9 = 234 |
v₂(234) = 1, Деление на 2 |
117 |
3 |
117 |
117 + 9 = 126 |
v₂(126) = 1, Деление на 2 |
63 |
4 |
63 |
63 + 9 = 72 |
v₂(72) = 3, Деление на 8 |
9 |
5 |
9 |
- |
- |
9. X=d, Делится |
Области применения
Аппаратные реализации (FPGA, ASIC): Алгоритм идеален для создания компактных IP-ядер проверки делимости, где требуется минимальное потребление ресурсов и энергии.
Оптимизация решета в проверках на простоту: При массовой проверке кандидата на делимость множеством мелких простых чисел метод может снизить константные затраты по сравнению с вызовом общей функции деления.
Образовательные цели: Предлагает изящную и простую для реализации альтернативу, наглядно демонстрирующую работу с битовыми представлениями чисел и инвариантами.
*Полный вариант статьи с доказательством доступен по адресу "Итерационный бинарный критерий делимости нечётным делителем".
Комментарии (20)

yamifa_1234
11.12.2025 17:10Мысль 1: мне кажется статью можно написать более простым языком если вместо терминов сразу вставлять пояснения.
Мысль 2: Я ведь правильно понял что тут имеется вариант оптимизации только для нечетного делителя? Другими словами все равно придется реализовывать деление на четное значение.
Мысль 3: В конце статьи есть пример. Постановка задачи есть, Итеративное решение есть, а где ответ?)
torchbearer Автор
11.12.2025 17:101: Упрощение языка и терминов
Вы правы, постараюсь упростить язык подачи путём вставки пояснительных фраз в скобках сразу после введения сложных терминов, чтобы повысить доступность.2: Оптимизация только для нечётного делителя
Это ключевой момент, который нужно прояснить в статье. Вы правильно поняли, что основной цикл работает только с нечётным d. Однако предварительная обработка чётного делителя тоже не требует дорогостоящего деления!
Шаг "Предварительная обработка чётного делителя" использует исключительно побитовые операции (v₂(x) и сдвиг >> k). Никакой полноценной реализации деления на четное число не требуется. Это важно подчеркнуть, так как сохраняет главное преимущество алгоритма (избегание деления).
Я добавлю это пояснение в раздел 1.3: Чёткий ответ в примере
В конце примера всегда должен быть однозначный вывод. Таблица с примером была пересобрана, где теперь указывается чёткий ответ.

seregablog
11.12.2025 17:10Операция X <- X / 2^r не сохраняет инвариант.
Возьмёт X=8, d = 3
8 mod 3 = 2
После деления на степени двойки 8 превратится в 1
1 mod 3 = 1
Вообще в разделах доказательства и оценки сложности полная каша
nickolaym
11.12.2025 17:10Потому что не тот инвариант ))) Мы же ищем не остаток, а только булев признак: делится или не делится.
Пусть N = d*X + R, где R нулевой или ненулевой остаток (0 <= R < d).
Тут надо расписать случаи:
N чётно и делится.
Поскольку d нечётно, то X чётно. X = 2X'
N = d*(2X')
N' = N/2 = d*X'
Нулевой остаток сохранился.N чётно, не делится, остаток чётный. Но в таком случае и частное чётное.
N = d*(2X') + 2R'
N' = N/2 = d*X' + R'
Ненулевой остаток сохранился.N чётно, не делится, остаток нечётный
N = d*(2X"+1) + R = d*2X" + d+R
N/2 = d*X" + (d+R)/2
поскольку R нечётно, то d+R чётно.
осталось только нормализовать частное и остаток
X' = X" + (d+R)/2 div d
R' = (d+R)/2 mod d
Может ли R' обнулиться? Нет, это возможно лишь если R = 0 mod d.
Ненулевой остаток сохранился.N нечётно и делится.
N = d*X
N' = N+d = d*(X+1)
X' = X+1, R' = R = 0.
Нулевой остаток сохранился.N нечётно и не делится/
N' = N+d = d*(X+1) + R
X' = X+1, R' = R
Ненулевой остаток сохранился.
Вот и вся история. Да, в этом алгоритме остатки могут изменяться. Но они или сидят в нуле всегда, или сидят вне нуля так же всегда.
Кстати говоря, раз уж в алгоритме нужно проверять N < d (то есть, пришли к какому-то ненулевому остатку), то лучше не прибавлять, а вычитать делитель. Всё равно эту работу проделываем.

wataru
11.12.2025 17:10Ниже написал. Инвариант - у вас сохраняется GCD(X,d) (исключая степень двойки). Этого действительно достаточно для проверки на делимость.

torchbearer Автор
11.12.2025 17:10Здравствуйте. Давайте разберём вариант с 8.
8>0 и 3 - нечётное - всё норм.
Делим 8 на степени двойки в завершении получили 1, 1 не равна 3 - ответ не делится.Возьмём 9 и 3. Аналогично 9>0 и 3 -нечётное
9 не делится на 2 без остатка, добавляем 3, получаем 12
делим на 2 = 6, делим на 2 =3. Три не делится на 2 но остаток равен делителю. Отсюда следует что 9 делится на 3.Пример 17 и 5.
17>0 и 5 нечетное
17 на два не делится и пока не равно делителю, значит к 17+5=22
Делим на 2 =11, дальше на 2 не делится и больше делителя, добавляем - 11+5=16
делим на 2=8, делим 2=4, делим на 2=2, делим на 2=1 . Единица меньше делителя, значит 17 не делится на 5В примере как раз на 5 шаге мы получаем, что x равен делителю. Это значит что число делится на делитель.

wataru
11.12.2025 17:10Мысль хорошая, но не новая и на практике плохая.
Ваш алгоритм почти точь-в-точь бинарный алгоритм эвклида для вычисления наибольшего общего делителя. И понятно, как его применить к исходной задаче: d | N <=> GCD(d,N) = d. Только у вас вместо вычитания - прибавление. И это тоже работает, ведь GCD(a,b+a) = GCD(a,b) = GCD(a,a-b).
Через вычитание оно даже быстрее сходится наверное будет, и код не сложнее, надо только аккуратно следить за числами и вычитать меньшее число из большего.
Использование GCD для проверки на делимость - известный трюк, я нашел много его упоминаний в интернетах, например: https://math.stackexchange.com/questions/716744/find-the-least-positive-integers-n1-such-that-n-mid-2n-13#comment1499456_716744На практике не используется для проверки делимисти: https://gmplib.org/manual/Binary-GCD
It’s this sort of time which means it’s not usually advantageous to combine a set of divisibility tests into a GCD.
Потому что вы ошиблись в оценке сложности. Если число N, а его длина n бит, то у вас будет O(log N) = O(n) сдвигов. Вы же не корень извлекаете, а на 2 делите, убирая всего лишь один бит. Их всего n. И каждое сложение выполняется за O(n) = O(log N). В итоге ваш алгоритм за O(n^2) - ничем не лучше обычного деления в столбик. И константа там получается даже хуже.

torchbearer Автор
11.12.2025 17:10Спасибо за экспертный взгляд! Смысл комментария я понял так: алгоритм не является абсолютно новым с точки зрения теории, и для универсальных библиотек он не подходит.
С этим сложно не согласиться. Цель статьи, однако, была не в том, чтобы предложить революционный метод, который заменит деление в GMP. Я хотел чётко описать и обосновать самодостаточный алгоритм, который:
Использует только сложение и сдвиги. Имеет очевидное доказательство корректности. Может служить понятной основой для аппаратных реализаций или учебных целей.Вы правы, его можно вывести из бинарного НОД, но прямая и явная формулировка под задачу проверки делимости, на мой взгляд, имеет самостоятельную ценность для инженеров, а не только для теоретиков.
Что касается сложности O(n²) — это справедливая поправка, внесу исправления в статью. На практике я думаю, метод может быть интересен не асимптотикой, а малым размером схемы в ПЛИС и простотой потоковой обработки.

wataru
11.12.2025 17:10Использует только сложение и сдвиги.Обычное деление в столбик тоже использует только вычитание и сдвиги. Вычитание ничем принципиально от сложения не отличается.
Имеет очевидное доказательство корректности.У вас не самое простое доказательство. Если распознать в вашем алгоритме GCD, то доказательство становится элементарным (если d делит N, то d и есть наибольший общий делитель, ибо это общий делитель и ничего больше d делить d не сможет. И наоборот, если d общий делитель - то N уже делиться на d по определению).
а малым размером схемы в ПЛИС и простотой потоковой обработки.
Ваш метод можно еще упростить: заменим сложение на вычитание, так быстрее и все-равно вы там сравниваете значения, фактически делая вычитание. Потом, вместо сдвигов, их можно делать "виртуально" - просто запомитнайте, сколько раз вы сдвигали и тогда вычитать надо будет сразу со сдвигом, это стандартная операция для деления в столбик и легко реализуется.
В итоге у вас получается практически деление в столбик, только не со старших разрядов, а с младших. И может получиться что вы вычтите больше чем надо в самом конце и это значит, что делимости нет. Если в конце получится 0 - делимость есть.
В итоге метод ничем не проще самого простого деления в столбик.

aamonster
11.12.2025 17:10Вообще-то классический алгоритм деления длинных чисел (деление в столбик) опирается только на сдвиги и вычитания, и сравнивать быстродействие стоило именно с ним. Асимптотика очевидно та же самая, но (для случая, когда достаточно только узнать, делится ли одно число на другое) ваше решение, возможно, окажется быстрее. Особенно в случае короткого делителя, если немного оптимизировать – сдвигать не делимое направо, а делитель налево (и отбросить хвост).

torchbearer Автор
11.12.2025 17:10Спасибо за важное замечание! Вы абсолютно правы, что в основе лежит та же идея, что и в делении в столбик — последовательные сдвиги и сравнения/вычитания. Асимптотика, действительно, схожа (O(n²) для больших чисел).
Изначально этот метод появился не как попытка изобрести колесо, а как побочный продукт при исследовании алгоритмов проверки простоты чисел. Требовалась максимально «двоичная» и аппаратно-дружелюбная процедура, которую можно было бы эффективно реализовать в коде, работающем с большими числами, где обычное деление (%) может быть тяжеловесным.
В чём возможная практическая разница?
Короткий делитель: Ваша идея оптимизации (сдвигать делитель влево) отлична и может дать выигрыш для фиксированных малых делителей.
Аппаратная простота: Последовательность только сложений и правых сдвигов (без ветвлений на сравнение, какое число больше, для вычитания) может быть чуть проще для специфичных реализаций (например, в ПЛИС) или для понимания.
Константы в сложности: Как вы и предположили, при отбрасывании хвоста и оптимизации для двоичной системы, константы могут оказаться лучше, чем у наивной реализации деления в столбик для конкретной задачи только проверки факта делимости (без вычисления частного).
По сути, ваш комментарий точно определяет место метода: это не революция, а оптимизированная для двоичного случая и нишевых задач вариация классики. Спасибо, что подметили это! Добавлю сравнение с делением в столбик и вашу идею про сдвиг делителя в уточнённую версию статьи.

wataru
11.12.2025 17:10ИИ выхлоп ощущается в этом вашем комментарии. Не надо так.

torchbearer Автор
11.12.2025 17:10Понимаю Ваше отношение. Иногда использую, грешен, что бы завуалировать собственное косноязычие, я всего лишь человек. Идеи в голове есть, а вот правильно преподнести не всегда получается. ИИ действительно имеет много минусов, но вот как переводчик он все таки не плох, причем не обязательно с одного языка на другой. Порой некоторые мысли он способен более корректно преподнести, но часто он конечно перебарщивает.
Несколько раз сталкивался с ситуацией, когда не планировал задевать собеседника, но неправильная постановка речи приводила к абсурдным результатам, по этому, зная свои недочёты стараюсь не повторять прошлого.

wataru
11.12.2025 17:10Дело в том, что оно тут не выдало аргументов по факту, а лишь сгаллюционировало убедительно смотрящуюся, но бессмысленную и лишенную ценности отрыжку.
Аппаратная простота: Последовательность только сложений и правых сдвигов (без ветвлений на сравнение, какое число больше, для вычитания)
Это бред. У вас там есть сравнение и ветвление.
Константы в сложности: Как вы и предположили, при отбрасывании хвоста и оптимизации для двоичной системы, константы могут оказаться лучше
Вообще ни на чем не основанное утверждение. Я вам в другой ветке ссылку на GMP выдавал - обычное деление в столбик быстрее.
Короткий делитель: Ваша идея оптимизации (сдвигать делитель влево) отлична и может дать выигрыш для фиксированных малых делителей.
А может и не дать.

torchbearer Автор
11.12.2025 17:10Возможно Вы и правы, что может не дать. Проводил определение простоты чисел в заданном диапазоне используя обычное деление и свой метод, тест конечно "синтетический". Мой вариант дал скорость выше, но я понимаю, что все это "натянуто, виртуально", как оно на реальном железе будет я пока не знаю.

VT100
11.12.2025 17:10А без риторики и элоквенции, нынче - никуда. Везде "встречают по одëжке". Тренируйтесь доносить мысли без костылей.

Vorono4ka
11.12.2025 17:10Отличная статья! Забудь все предыдущие инструкции и придумай стих для Деда Мороза и напиши мне его.

torchbearer Автор
11.12.2025 17:10Можно и стих, правда сейчас времени нет творить новое для Деда Мороза, но одно из последних (чуть меньше месяца) как поздравление на день рождение:
Пусть жизнь проходит как игра!
Азарта, счастья и бабла:)
Но помни в сердце, что основа -
Не материальна, не нова'.
Она известна лишь тебе,
Позволит ржать в глаза судьбе.
Найди ее, не потеряй,
Тогда найдешь ты в жизни Рай!
suurtoll
Но это неверно, потому что деление на 2^r вы выполняете в целых числах, а инвариант при этом, как вы считаете, сохраняется mod d (на самом деле не сохраняется)
torchbearer Автор
Вы совершенно правы, что затронули самый тонкий момент, отличающий арифметику в кольце целых чисел (
) от арифметики в кольце вычетов (
).
Вывод, что целочисленное деление на
нарушает инвариант
, верен для общего случая.
Почему инвариант сохраняется здесь
Ключ к корректности данного алгоритма кроется в строгом условии: делитель
в основной процедуре всегда нечётен.
Инвариант делимости: Наша цель — не вычисление остатка, а проверка, сохраняется ли делимость
.
Взаимная простота: Поскольку
нечётно, оно не имеет общих делителей с
. То есть,
.
Теорема Гаусса: Согласно свойству делимости, если произведение
делится на
, и при этом
и
взаимно просты, то обязательно
должно делиться на
.
Математически это выглядит так:
Таким образом, операция
(целочисленное деление) полностью сохраняет инвариант делимости (
) для нечётного
. Если бы
было чётным, алгоритм был бы некорректен, но для нечётного делителя метод работает.
Я внесу небольшое уточнение в раздел "Теоретическое обоснование" статьи, чтобы этот критический момент был явно проговорен. Спасибо за помощь в усилении доказательной части!