Привет, Хабр!
Операция проверки делимости — одна из самых фундаментальных в информатике и теории чисел. Для обычных чисел, помещающихся в машинное слово, это одна быстрая аппаратная инструкция. Но для очень больших целых чисел (Big Integers), размер которых превышает разрядность регистра процессора, деление и взятие остатка становится ресурсоёмкой многословной процедурой.
Эта работа предлагает новый детерминированный алгоритм для проверки делимости целого числа на нечётный делитель
. Его ключевая особенность: он использует исключительно операции сложения (
) и деления на 2 (побитового сдвига вправо,
), что позволяет полностью избежать дорогой операции взятия остатка.
Основная идея и ценность
Традиционная проверка делимости сводится к вычислению остатка
. Данный подход итеративно преобразует число
в меньшее число
, сохраняя при этом инвариант делимости:
.
Основная практическая ценность заключается в эффективности для больших целых чисел (Big Integers) и аппаратно-ориентированных реализаций (FPGA/ASIC), поскольку сложение и сдвиг являются существенно более дешёвыми операциями, чем деление.
Сравнение сложности (для Big Integers)
Характеристика |
Традиционный |
Итерационный бинарный критерий |
|---|---|---|
Ключевые операции |
Деление (дорого) |
Сложение и Сдвиг (дёшево) |
Сложность для Big Int |
O(n²) или O(n log n) |
O( n log n) |
Эффективность |
Низкая для Big Integers |
Асимптотически выше для больших |
В чем практическое преимущество?
Несмотря на схожесть асимптотических оценок сложности, преимущество алгоритма кроется в типе используемых операций.
Традиционный метод включает дорогостоящие многословные процедуры деления или умножения, которые, даже при асимптотике
, имеют большой константный накладной расход.
Данный алгоритм заменяет эту дорогую операцию на
итераций, каждая из которых состоит только из сложения и побитового сдвига.Это самые быстрые и простые операции с линейной сложностью
. *Это обеспечивает радикальное снижение константного фактора времени выполнения, делая алгоритм особенно эффективным для аппаратных платформ (FPGA/ASIC), где блоки сложения и сдвига требуют меньше логических элементов и энергии, чем блоки деления.
Алгоритм: Шаги и Псевдокод
Алгоритм сначала сводит задачу к случаю, когда делитель — нечётный.
1. Предварительная обработка чётного делителя
Если чётно, оно представляется в виде:
Пусть (x) обозначает показатель максимальной степени двойки, делящей
(количество младших нулей).
Сначала проверяется, делится ли на
: если
то не делится на
.
Иначе, производится редукция:
Далее проверяется делимость на нечётное
.
2. Основная процедура для нечётного делителя
Входные параметры:
Инициализация:
В цикле выполняются:
Нормализация по 2:
делится на 2 (сдвиг вправо), пока не станет нечётным:
/2
-
Шаг итерации (если
):
Псевдокод: Итерационный бинарный критерий делимости
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): количество младших нулей (CTZ)
if v2(N) < k: // Проверка делимости N на 2^k
return False
N = N >> k // N = N / 2^k
d = d >> k // d = d / 2^k (Теперь d нечётно)
// 2. Основная процедура для нечётного d
X = N // Инициализация
while True:
// 2.1. Нормализация (Удаление всех факторов 2 из X)
r = v2(X) // Подсчёт младших нулей
if r > 0:
X = X >> r // X = X / 2^r
// 2.2. Проверка условий останова
if X == d: // Если X = d, возвращаем True
return True
if X < d: // Если X < d, возвращаем False
return False
// 2.3. Выполнение шага итерации (X <- X + d)
X = X + d // Переход к шагу 2.1.
? Теоретическое обоснование
На каждой итерации алгоритма сохраняется ключевой инвариант:
Поскольку нечётно,
. Операции
и
(деление на степень двойки) сохраняют инвариант, так как деление на
является обратимым преобразованием в кольце вычетов
.
Сложность алгоритма составляет
где — количество машинных слов в числе, а
— количество итераций цикла.
Пример работы
Проверим делимость на
.
Шаг |
X (Нечётное) |
Итерация: |
Нормализация ( |
Результат X |
|---|---|---|---|---|
0 |
3519 |
|||
1 |
441 |
|||
2 |
225 |
|||
3 |
117 |
|||
4 |
63 |
|||
5 |
9 |
- |
- |
9 |
Области применения
Библиотеки для работы с большими числами (GMP, OpenSSL): Замена многословного деления на многословное сложение и сдвиг.
Аппаратные реализации (FPGA, ASIC): Идеально для специализированных IP-ядер, так как сложение и сдвиг требуют меньше логических элементов и энергии, чем полноценный блок деления.
*Полный вариант статьи с доказательством доступен по адресу "Итерационный бинарный критерий делимости нечётным делителем".
Дополнительный плюс, что этот процесс поддаётся распараллеливанию. Как вариант можно применять при проверке чисел на простоту.
Комментарии (7)

yamifa_1234
11.12.2025 17:10Мысль 1: мне кажется статью можно написать более простым языком если вместо терминов сразу вставлять пояснения.
Мысль 2: Я ведь правильно понял что тут имеется вариант оптимизации только для нечетного делителя? Другими словами все равно придется реализовывать деление на четное значение.
Мысль 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) (исключая степень двойки). Этого действительно достаточно для проверки на делимость.

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) - ничем не лучше обычного деления в столбик. И константа там получается даже хуже.

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