Оптимизация кода в рамках задачи «Делители»

Всем здравствуйте! Есть код для решения задачи, но он работает слишком медленно. Нужно уложится в 1 секунду, а программа работает на ~1.1 секунду. Возможно, нужно как-то по-другому решить задачу, но не понимаю, как. Помогите, пожалуйста.

Условие задачи:
Дано натуральное число ?. Подсчитайте количество таких пар чисел (? и ?), что: ? и ? — делители ?; ? < ?; ? и ? — взаимно простые; ?? ≤ ?.

Выходные данные:
Вводится натуральное число ? ≤ 10**8.

Выходные данные:
Выведите количество таких пар.

Код:

from math import gcd

def full_factorization(n):
    p = []
    d = 2
    while d * d <= n:
        while n % d == 0:
            p.append(d)
            n //= d
        d += 1
    if n > 1:
        p.append(n)
    return p


def dividers(n):
    p = full_factorization(n)
    divs = []
    for i in range(1, n + 1):
        if n%i == 0:
            divs.append(i)
    return divs

def main(n):
    used = []
    number = 0
    div = dividers(n)
    for a in div:
        for b in div:
            if a < b and (a,b) not in used and gcd(a,b) == 1 and a*b <= n:
                number += 1
            used += [(a, b), (b, a)]
    return number
n = int(input())
print(main(int(input())))

Ответы (2 шт):

Автор решения: MiniMax

Замечание по вашему коду:

def dividers(n):
    p = full_factorization(n) # 'p' нигде не используется
    divs = []
    for i in range(1, n + 1):
        if n%i == 0:
            divs.append(i)
    return divs

Моё решение:

Для нахождения множителей числа можно применить более быстрый алгоритм, видео с объяснением на английском - Finding factors of a number.

from math import gcd

def solve(num):
    factors = []
    a = 1
    b = num
    while a < b:
        b = num // a
        if b * a == num:
            factors.append(a)
            # Обработка случая равных множителей, например 'a' = 10 и 'b' 10,
            # при num = 100.
            if a != b:
                factors.append(b)
        a += 1

    factors.sort()
    # Для наглядности
    print("factors", factors, '\n')

    cnt = 0
    # Проверяем все комбинации множителей.
    for i, a in enumerate(factors, 1):
        # Так как список factors отсортирован и состоит из уникальных значений,
        # любое число, находящееся правее текущего, будет больше, 
        # что даёт нам выполнение условия 'a < b'.
        for b in factors[i:]:
            # Если встретилось 'b', которое при умножении на 'a'
            # даёт произведение больше, чем 'num', то текущий цикл
            # можно завершать, так как дальше 'b' будет только увеличиваться.
            if a * b > num:
                break

            if gcd(a, b) == 1:
                # Для наглядности
                print("pair\t", a, b)
                cnt += 1

    return cnt

inpt = 100
print("\nanswer", solve(inpt))

Output

factors [1, 2, 4, 5, 10, 20, 25, 50, 100] 

pair     1 2
pair     1 4
pair     1 5
pair     1 10
pair     1 20
pair     1 25
pair     1 50
pair     1 100
pair     2 5
pair     2 25
pair     4 5
pair     4 25

answer 12
→ Ссылка
Автор решения: Stanislav Volodarskiy

Вы не укладываетесь по времени из-за медленной функции dividers. Она перебирает n чисел чтобы выделить из них делители n. Её можно ускорить, если правильно воспользоваться разложением из full_factorization. Всё остальное работает достаточно быстро потому что делителей мало: любое число n ≤ 108 имеет не более 768 делителей. Но я хочу предложить другой путь, который совмещает производительность O(√n) с относительной простотой кода - не нужно строить все делители чтобы найти ответ.

Обозначим искомое число пар множителей за f(n). Из набора условий ограничивающих пары для f уберём требование a < b и обозначим новое количество пар множителей через g(n). Заметим что каждая пара (a, b) для f соответствует двум парам для g, а именно парам (a, b) и (b, a). Также в g входит одна особенная пара - (1, 1), которой нет соответствия среди пар f. Это неформальное рассуждение приводит к формуле 2f(n) + 1 = g(n).

Дальше будем заниматься вычислением g(n).

Пусть n1 и n2 взаимно простые. Покажем что g(n1)g(n2) = g(n1n2).

Пусть (a1, b1) - пара множителей для числа n1, а (a2, b2) - пара множителей для числа n2. Рассмотрим пару (a1a2, b1b2). Множители в ней взаимно просты, следовательно эта пара входит в g(n1n2).

Обратно. Возьмём любую пару (a, b) из g(n1n2). a разлагается в произведение a = a1a2, где n1 : a1, n2 : a2. Разложение существует и единственно, так как n1 и n2 взаимно простые. Разложим аналогично b: b = b1b2, где n1 : b1, n2 : b2. Пара (a1, b1) соответствует g(n1), пара (a2, b2) соответствует g(n2).

Так как отображение ((a1, b1), (a2, b2)) ↔ (a, b) взаимнооднозначное, мощности множеств слева и справа равны. Слева произведение g(n1)g(n2), справа g(n). Равенство доказано.

Это свойство g называется мультипликативностью. Мультипликативные функции можно вычислять по разложению числа на простые. Заметим что если p - простое, то g(pa) = 2a + 1. Проверяется перечислением всех возможных пар. Тогда общая формула для разложения на простые будет:

g(∏piai) = ∏(2ai + 1), где pi - различные простые.

С теорией покончили. Практика: по разложению n на простые вычислить g(n), затем восстановить f(n) = (g(n) - 1)/2.

Функция g основана на общем алгоритме факторизации числа: сперва из числа удаляется степень двойки, затем проверяется делимость на нечётные числа. Если делится, соответствующий делитель оказывается простым и мы его тоже удаляем из числа. Цикл делает не более (√n)/2 итераций.

import math


def g(n):

    def factor(n, g, p):
        f = 1
        while n % p == 0:
            n //= p
            f += 2
        return n, g * f

    n, g = factor(n, 1, 2)
    i = 3
    n_sqrt = math.isqrt(n)
    while i <= n_sqrt:
        if n % i == 0:
            n, g = factor(n, g, i)
            n_sqrt = math.isqrt(n)
        i += 2
    if n > 1:
        n, g = factor(n, g, n)
    return g


def f(n):
    return (g(n) - 1) // 2


print(f(int(input())))

Работает для n ≤ 108 мгновенно. Цикл в функции g выполняется не более 5000 раз.

$ echo 9699690 | python f.py
3280

$ echo 67108864 | python f.py
26

$ echo 99999989 | python f.py
1

$ echo 91891800 | python f.py
9922
→ Ссылка