EduBrick
классная работа

Делимость и делители

Программирование на Python: от нуля до олимпиад

0/18решено 0 из 18до зачёта осталось 10

Чему научитесь

  • Сводить любой вопрос о делимости к остатку
  • Искать делители за корень и не терять пару у квадрата
  • Считать НОД алгоритмом Евклида и НОК через него
  • Группировать числа по остатку вместо перебора пар

Как устроено занятие

Сначала разбор с примерами и короткими проверками понимания — они нужны, чтобы поймать непонятое сразу, а не через три темы.

Дальше 15 задач лестницей: разминка, основа, со звёздочкой. Занятие засчитывается, когда решено 10 — остальные не пропадают и учитываются отдельно.

После занятия — вторая часть, ещё 15 задач на те же приёмы в новых сюжетах.

Сколько это займёт

Примерно час-полтора вместе с задачами. Сроков нет: можно закрыть вкладку и вернуться когда удобно — прогресс сохранится.

теория

Остаток решает всё

Школьные признаки делимости — «на 3, если сумма цифр делится на 3» — в программировании почти не нужны. У нас есть операция остатка, и любой вопрос о делимости сводится к ней:

if n % 3 == 0:
    print("делится на 3")

Никакой суммы цифр, никаких особых случаев для 7 и 11. Одна операция для любого делителя.

Две операции, связанные одним равенством

Целочисленное деление // и остаток % всегда согласованы:

a=(a//b)b+amodba = (a // b) \cdot b + a \bmod b

Это равенство держится всегда, и из него следует всё остальное.

Отрицательные числа: осторожно

Здесь Python ведёт себя не так, как C++ или Java, и на этом регулярно попадаются.

-7 // 3     # -3, а не -2 — деление округляется вниз, а не к нулю
-7 % 3      #  2, а не -1 — остаток всегда неотрицательный

Проверим равенство: (3)3+2=7(-3) \cdot 3 + 2 = -7. Сходится.

Практический вывод: в Python остаток от деления на положительное число никогда не бывает отрицательным. Это удобно — можно смело использовать a % k как номер ячейки в списке, не боясь получить минус. В C++ такой код упал бы.

теория

Делители ходят парами

Найти все делители числа 101210^{12} перебором до самого числа — триллион действий, то есть часы. К счастью, перебирать столько не нужно.

Ключевое наблюдение. Если ii делит nn, то и n/in / i делит nn. Делители разбиваются на пары, и в каждой паре один множитель не больше n\sqrt{n}, а другой не меньше.

Значит достаточно перебрать ii до корня — второй делитель каждой пары получится бесплатно:

i = 1
while i * i <= n:
    if n % i == 0:
        # нашли пару: i и n // i
        ...
    i += 1

Было 101210^{12} шагов, стало 10610^6. Это доли секунды.

Пара, которая совпадает сама с собой

Единственная тонкость — полные квадраты. У числа 36 пара (6,6)(6, 6) состоит из одного и того же делителя, и учитывать его надо один раз, а не два:

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) — она целочисленная и точная.

тест

Проверка: сколько шагов

Сколько примерно шагов нужно, чтобы найти все делители числа 101210^{12}?

Войдите, чтобы ответить.
теория

НОД и алгоритм Евклида

Наибольший общий делитель двух чисел ищется способом, которому больше двух тысяч лет. В основе — одно равенство:

НОД(a,b)=НОД(b,amodb)\text{НОД}(a, b) = \text{НОД}(b, a \bmod b)

Почему оно верно? Любой общий делитель aa и bb делит и их разность, а значит делит остаток amodba \bmod b. И наоборот. Значит у пар (a,b)(a, b) и (b,amodb)(b, a \bmod b) множества общих делителей совпадают целиком — а раз так, совпадают и наибольшие.

while b != 0:
    a, b = b, a % b
print(a)          # когда второе стало нулём, ответ в первом

Работает это очень быстро: остаток убывает стремительно, шагов получается порядка log\log от меньшего числа. Даже для восемнадцатизначных чисел — несколько десятков итераций.

В Python писать это руками не нужно: math.gcd(a, b) уже есть. Но знать, почему оно работает, — нужно.

НОК через НОД

НОК(a,b)=abНОД(a,b)\text{НОК}(a, b) = \frac{a \cdot b}{\text{НОД}(a, b)}

Записывать это стоит так:

lcm = a // gcd(a, b) * b        # сначала делим, потом умножаем

В Python целые числа неограниченные, поэтому a * b // gcd(a, b) тоже сработает. Но привычку делить первым лучше завести сейчас: в C++ произведение двух миллиардов уже не помещается в 64 бита, а после деления — помещается.

Для нескольких чисел

И НОД, и НОК собираются по одному числу за раз: НОД(a, b, c) = НОД(НОД(a, b), c). То же и для НОК.

расчёт

Проверка: делители числа 36

Сколько натуральных делителей у числа 36? Введите целое число.

Войдите, чтобы ответить.
теория

Остаток как ключ

Есть приём, который выглядит несерьёзно, а решает целый класс задач: группировка по остатку.

Задача: сколько в списке пар чисел, сумма которых делится на kk?

Перебор пар — до двадцати миллиардов проверок. Но сумма делится на kk тогда и только тогда, когда остатки слагаемых дают в сумме kk или ноль. А остатков всего kk штук.

counts = [0] * k
for value in a:
    counts[value % k] += 1

Теперь ответ собирается из количеств: числа с остатком rr образуют пары с числами с остатком krk - r. Отдельно считаются два случая — остаток 0 и, если kk чётное, остаток k/2k/2: там пары составляются внутри одной группы, и их количество равно c(c1)2\frac{c(c-1)}{2}.

Тот же приём работает для разностей: разность делится на kk ровно тогда, когда у чисел одинаковый остаток.

Обратите внимание, чем мы заплатили: временем O(n+k)O(n + k) вместо O(n2)O(n^2) и списком на kk ячеек. Это типичный обмен для задач на делимость — перебор заменяется подсчётом по группам.

теория

Типичные ошибки

Квадрат посчитан дважды. Разобран выше. Проверяйте решение на 36, 49, 100.

Перебор до nn вместо корня. Проходит на маленьких тестах и получает превышение времени на больших. Признак: решение верное, а вердикт по времени.

Забыт случай n=1n = 1. У единицы ровно один делитель, наименьшего делителя больше единицы у неё нет, а наибольшего собственного — тоже. Такие задачи почти всегда содержат n=1n = 1 в тестах.

НОД нуля. gcd(0, x) = x, и это не ошибка, а определение: на ноль делится всё. А gcd(0, 0) = 0. Если в задаче встречаются нули, проверьте, что ваш цикл их переживает.

НОК без деления первым. В Python не страшно, в других языках — переполнение. Заводите привычку сразу.

Пустой список для остатков. counts = [0] * k при k=1k = 1 даёт список из одной ячейки, и все числа попадают в неё. Это верно, но проверьте, что формулы с krk - r и k/2k/2 на таком kk не выходят за границы.

Как проверять себя

  • n=1n = 1 и nn простое;
  • полный квадрат: 36, 49, 100;
  • число с очень многими делителями: у 963 761 198 400 их 6720;
  • k=1k = 1 в задачах на остатки — тогда подходит вообще всё.
теория

Практика: пятнадцать задач

Лестница прежняя: пять разминочных, семь основных, три со звёздочкой. Зачёт при десяти решённых.

Почти везде работают три инструмента: остаток, перебор до корня и НОД. Если задача выглядит так, будто нужен перебор до nn, — почти наверняка нужен перебор до корня.

И следите за границами: числа в этом занятии доходят до 101810^{18}. В Python это не проблема, но проверьте, что вы не считаете лишнего.

лестница задач
развёрнутый ответ

Делитель, посчитанный дважды

Задача: «дано nn от 1 до 101210^{12}; выведите количество его натуральных делителей».

Ученик написал:

n = int(input())

count = 0
i = 1
while i * i <= n:
    if n % i == 0:
        count += 2
    i += 1

print(count)

Перебор до корня сделан правильно, по времени решение проходит. На числах 12, 30 и 1000 ответ верный.

Найдите вход, на котором решение ошибается, объясните причину и предложите исправление.

Войдите, чтобы ответить.