Чему научитесь
- Сводить любой вопрос о делимости к остатку
- Искать делители за корень и не терять пару у квадрата
- Считать НОД алгоритмом Евклида и НОК через него
- Группировать числа по остатку вместо перебора пар
Как устроено занятие
Сначала разбор с примерами и короткими проверками понимания — они нужны, чтобы поймать непонятое сразу, а не через три темы.
Дальше 15 задач лестницей: разминка, основа, со звёздочкой. Занятие засчитывается, когда решено 10 — остальные не пропадают и учитываются отдельно.
После занятия — вторая часть, ещё 15 задач на те же приёмы в новых сюжетах.
Сколько это займёт
Примерно час-полтора вместе с задачами. Сроков нет: можно закрыть вкладку и вернуться когда удобно — прогресс сохранится.
Остаток решает всё
Школьные признаки делимости — «на 3, если сумма цифр делится на 3» — в программировании почти не нужны. У нас есть операция остатка, и любой вопрос о делимости сводится к ней:
if n % 3 == 0:
print("делится на 3")
Никакой суммы цифр, никаких особых случаев для 7 и 11. Одна операция для любого делителя.
Две операции, связанные одним равенством
Целочисленное деление // и остаток % всегда согласованы:
Это равенство держится всегда, и из него следует всё остальное.
Отрицательные числа: осторожно
Здесь Python ведёт себя не так, как C++ или Java, и на этом регулярно попадаются.
-7 // 3 # -3, а не -2 — деление округляется вниз, а не к нулю
-7 % 3 # 2, а не -1 — остаток всегда неотрицательный
Проверим равенство: . Сходится.
Практический вывод: в Python остаток от деления на положительное число никогда не бывает отрицательным. Это удобно — можно смело использовать a % k как номер ячейки в списке, не боясь получить минус. В C++ такой код упал бы.
Делители ходят парами
Найти все делители числа перебором до самого числа — триллион действий, то есть часы. К счастью, перебирать столько не нужно.
Ключевое наблюдение. Если делит , то и делит . Делители разбиваются на пары, и в каждой паре один множитель не больше , а другой не меньше.
Значит достаточно перебрать до корня — второй делитель каждой пары получится бесплатно:
i = 1
while i * i <= n:
if n % i == 0:
# нашли пару: i и n // i
...
i += 1
Было шагов, стало . Это доли секунды.
Пара, которая совпадает сама с собой
Единственная тонкость — полные квадраты. У числа 36 пара состоит из одного и того же делителя, и учитывать его надо один раз, а не два:
if n % i == 0:
count += 1 if i * i == n else 2
Забыть эту строчку — самая частая ошибка темы. Она проявляется ровно на квадратах, а их среди чисел до миллиона всего тысяча — легко не заметить на случайных тестах.
Сравнивайте i * i <= n, а не i <= n ** 0.5
Второй вариант считает вещественный корень, и вопрос «а точно ли он вычислен верно на восемнадцатизначном числе» приходится обдумывать отдельно. С i * i <= n вопрос не возникает вовсе: там только целые числа. Если корень всё же нужен явно, берите math.isqrt(n) — она целочисленная и точная.
Проверка: сколько шагов
Сколько примерно шагов нужно, чтобы найти все делители числа ?
НОД и алгоритм Евклида
Наибольший общий делитель двух чисел ищется способом, которому больше двух тысяч лет. В основе — одно равенство:
Почему оно верно? Любой общий делитель и делит и их разность, а значит делит остаток . И наоборот. Значит у пар и множества общих делителей совпадают целиком — а раз так, совпадают и наибольшие.
while b != 0:
a, b = b, a % b
print(a) # когда второе стало нулём, ответ в первом
Работает это очень быстро: остаток убывает стремительно, шагов получается порядка от меньшего числа. Даже для восемнадцатизначных чисел — несколько десятков итераций.
В Python писать это руками не нужно: math.gcd(a, b) уже есть. Но знать, почему оно работает, — нужно.
НОК через НОД
Записывать это стоит так:
lcm = a // gcd(a, b) * b # сначала делим, потом умножаем
В Python целые числа неограниченные, поэтому a * b // gcd(a, b) тоже сработает. Но привычку делить первым лучше завести сейчас: в C++ произведение двух миллиардов уже не помещается в 64 бита, а после деления — помещается.
Для нескольких чисел
И НОД, и НОК собираются по одному числу за раз: НОД(a, b, c) = НОД(НОД(a, b), c). То же и для НОК.
Проверка: делители числа 36
Сколько натуральных делителей у числа 36? Введите целое число.
Остаток как ключ
Есть приём, который выглядит несерьёзно, а решает целый класс задач: группировка по остатку.
Задача: сколько в списке пар чисел, сумма которых делится на ?
Перебор пар — до двадцати миллиардов проверок. Но сумма делится на тогда и только тогда, когда остатки слагаемых дают в сумме или ноль. А остатков всего штук.
counts = [0] * k
for value in a:
counts[value % k] += 1
Теперь ответ собирается из количеств: числа с остатком образуют пары с числами с остатком . Отдельно считаются два случая — остаток 0 и, если чётное, остаток : там пары составляются внутри одной группы, и их количество равно .
Тот же приём работает для разностей: разность делится на ровно тогда, когда у чисел одинаковый остаток.
Обратите внимание, чем мы заплатили: временем вместо и списком на ячеек. Это типичный обмен для задач на делимость — перебор заменяется подсчётом по группам.
Типичные ошибки
Квадрат посчитан дважды. Разобран выше. Проверяйте решение на 36, 49, 100.
Перебор до вместо корня. Проходит на маленьких тестах и получает превышение времени на больших. Признак: решение верное, а вердикт по времени.
Забыт случай . У единицы ровно один делитель, наименьшего делителя больше единицы у неё нет, а наибольшего собственного — тоже. Такие задачи почти всегда содержат в тестах.
НОД нуля. gcd(0, x) = x, и это не ошибка, а определение: на ноль делится всё. А gcd(0, 0) = 0. Если в задаче встречаются нули, проверьте, что ваш цикл их переживает.
НОК без деления первым. В Python не страшно, в других языках — переполнение. Заводите привычку сразу.
Пустой список для остатков. counts = [0] * k при даёт список из одной ячейки, и все числа попадают в неё. Это верно, но проверьте, что формулы с и на таком не выходят за границы.
Как проверять себя
- и простое;
- полный квадрат: 36, 49, 100;
- число с очень многими делителями: у 963 761 198 400 их 6720;
- в задачах на остатки — тогда подходит вообще всё.
Практика: пятнадцать задач
Лестница прежняя: пять разминочных, семь основных, три со звёздочкой. Зачёт при десяти решённых.
Почти везде работают три инструмента: остаток, перебор до корня и НОД. Если задача выглядит так, будто нужен перебор до , — почти наверняка нужен перебор до корня.
И следите за границами: числа в этом занятии доходят до . В Python это не проблема, но проверьте, что вы не считаете лишнего.
Делитель, посчитанный дважды
Задача: «дано от 1 до ; выведите количество его натуральных делителей».
Ученик написал:
n = int(input())
count = 0
i = 1
while i * i <= n:
if n % i == 0:
count += 2
i += 1
print(count)
Перебор до корня сделан правильно, по времени решение проходит. На числах 12, 30 и 1000 ответ верный.
Найдите вход, на котором решение ошибается, объясните причину и предложите исправление.