Нужно ускорить выполнение кода

Найдите все натуральные числа, принадлежащие отрезку [35000000;40000000], у которых ровно пять различных нечётных делителей (количество чётных делителей может быть любым). В ответе перечислите найденные числа в порядке возрастания.

def d(n):
    lst = []
    for i in range(1, int(n**0.5) + 1):
        if n % i == 0:
            if i % 2:
                lst.append(i)
            if (n // i) % 2:
                lst.append(n // i)
    if len(set(lst)) == 5:
        return n
    return 0
lst1 = []
for i in range(35000000, 40000000 + 1):
    lst1.append(d(i))
print(set(lst1))

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

Автор решения: Stanislav Volodarskiy

Долго ли это работает? Я взял ваш код и стал менять в нём диапазон:

Изменение кода время прогона
for i in range(35000000,35000000+1): 0.009с
for i in range(35000000,35000000+10): 0.022с
for i in range(35000000,35000000+1000): 0.147с
for i in range(35000000,35000000+10000): 1.42с
for i in range(35000000,35000000+100000): 13.9с

Видно что время меняется примерно пропорционально количеству чисел, которые надо проверить. Всего надо проверить пять миллионов. Сто тысяч проверяются за 14с. Ожидаемое время работы 50·14 = 700с. Двенадцать минут и готово. Не устраивает?

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

for i in range(35000000,40000000+1):
    if d(i) != 0:
        print(i)

У большинства чисел делителей больше чем пять. А функция d накапливает их все, хотя могла бы остановиться значительно раньше. Надо позаботится чтобы в списке lst не было повторений. Если подумать, то делители вида i повторяться не могут. Делители вида n // i тоже. Они могут пересечься, если i == i // n. Проверим это:

def d(n):
    lst = []
    for i in range(1,int(n**0.5)+1):
        if n % i == 0:
            if i % 2 != 0:
                lst.append(i)
            if (n // i != i) and ((n // i) % 2 != 0):
                lst.append(n // i)
            if len(lst) > 5:
                break
    if len(lst) == 5:
        return n
    return 0

С этим изменением сто тысяч чисел обработаются за 6.25с. То есть чуть больше пяти минут для всего диапазона.

Делители проверяются на чётность и это правильно. Но можно схитрить: уберём из n все двойки. То есть будем его делить пополам, пока делится. Нечётный остаток будет иметь только нечётные делители, можно сэкономить на проверках и сам цикл станет короче:

def d(n):
    while n % 2 == 0:
        n //= 2
    lst = []
    for i in range(1,int(n**0.5)+1):
        if n % i == 0:
            lst.append(i)
            if n // i != i:
                lst.append(n // i)
            if len(lst) > 5:
                break
    if len(lst) == 5:
        return n
    return 0

С этим изменением сто тысяч чисел обработаются за 4.64с. Меньше четырёх минут для полного диапазона.

Раз число теперь всегда нечётное то у него не может быть чётных делителей. А мы их проверяем. В вызов range добавим шаг чтобы он перебирал только нечётные делители: range(1,int(n**0.5)+1,2).

С этим изменением сто тысяч чисел обработаются за 2.4с. То есть две минуты для всего диапазона. Эту программу я запустил и получилось чуть дольше, но не намного:

$ time py temp.py
35819648
38950081
39037448
39337984

real    2m12.187s
user    2m12.163s
sys 0m0.012s

P.S. Код можно ещё улучшать, но как его ускорить ещё больше я не придумал.

P.P.S. Если поменять алгоритм с простой проверки делителей на подход похитрее, можно решить задачу мгновенно. Но это уже за рамками вопроса "как ускорить мой код".

→ Ссылка
Автор решения: Fox Fox
import os
import time

print("-" * 50 +'''\nНатуральные числа, принадлежащие отрезку [35000000;40000000], у которых ровно 5 различных нечётных делителей\n''' +"-" * 50)

# Засекаем время старта
v_start_time = time.time()

# Функция: проверка, является ли число простым (достаточно до sqrt)
def is_prime(v_n):
 if v_n < 2:
  return False
 for v_i in range(2, int(v_n ** 0.5) + 1):
  if v_n % v_i == 0:
   return False
 return True

# Список для хранения результатов
lst_results = []

# Перебираем все простые числа p, такие что p^4 * 2^k попадает в диапазон
for v_p in range(3, 1000, 2):  # 1000 — с запасом, т.к. 999^4 > 1e12
 if is_prime(v_p):
  v_p4 = v_p ** 4  # Вычисляем p^4

  v_n = v_p4
  while v_n <= 40_000_000:
   if v_n >= 35_000_000:
    # Получаем и сортируем нечётные делители (всегда 5 штук)
    lst_divs = [1, v_p, v_p**2, v_p**3, v_p**4]
    lst_results.append((v_n, lst_divs))
   v_n *= 2  # Умножаем на степень двойки

# Засекаем время окончания
v_end_time = time.time()

# Выводим время выполнения
print("Время выполнения: {:.6f} секунд".format(v_end_time - v_start_time))

# Выводим найденные числа и их нечётные делители
for v_n, lst_divs in sorted(lst_results):
 print(v_n, lst_divs)

os.system("pause > nul" if os.name == "nt" else "read > /dev/null")

Результат:

Время выполнения: 0.000315 секунд
35819648 [1, 23, 529, 12167, 279841]
38950081 [1, 79, 6241, 493039, 38950081]
39037448 [1, 47, 2209, 103823, 4879681]
39337984 [1, 7, 49, 343, 2401]
→ Ссылка
Автор решения: Pak Uula

UPDATE

Обновлённый бенчмарк. Дабы не загромождать ответ, код выложен здесь: https://pastebin.com/KjRWqMDT

Результаты:

# Testing with left=35_000_000, right=40_000_000
Solver          : 0.469784 seconds for 25930 iterations average: 0.018117ms
Fox             : 0.861001 seconds for 35772 iterations average: 0.024069ms
Volodarski2     : 1.127676 seconds for 118149 iterations        average: 0.009545ms
# Testing with left=35_000_000_000, right=40_000_000_000
Solver          : 0.763839 seconds for 6168 iterations  average: 0.123839ms
Fox             : 0.792939 seconds for 4877 iterations  average: 0.162587ms
Volodarski2     : 1.071768 seconds for 25692 iterations average: 0.041716ms
# Testing with left=35_000_000_000_000, right=40_000_000_000_000
Solver          : 0.846638 seconds for 1136 iterations  average: 0.745280ms
Fox             : 1.019128 seconds for 921 iterations   average: 1.106545ms
Volodarski2     : 0.834278 seconds for 3971 iterations  average: 0.210093ms
# Testing with left=35_000_000_000_000_000, right=40_000_000_000_000_000
Solver          : 1.018586 seconds for 212 iterations   average: 4.804652ms
Fox             : 1.005992 seconds for 107 iterations   average: 9.401796ms
Volodarski2     : 1.057230 seconds for 847 iterations   average: 1.248205ms
# Testing with left=35_000_000_000_000_000_000, right=40_000_000_000_000_000_000
Solver          : 0.905872 seconds for 19 iterations    average: 47.677492ms
Fox             : 0.432845 seconds for 5 iterations     average: 86.568975ms
Volodarski2     : 0.954767 seconds for 131 iterations   average: 7.288296ms
# Testing with left=35_000_000_000_000_000_000_000, right=40_000_000_000_000_000_000_000
Solver          : 0.585327 seconds for 2 iterations     average: 292.663455ms
Fox             : 0.929334 seconds for 1 iterations     average: 929.334402ms
Volodarski2     : 0.965677 seconds for 22 iterations    average: 43.894432ms
# Testing with left=35_000_000_000_000_000_000_000_000, right=40_000_000_000_000_000_000_000_000
Solver          : 1.803623 seconds for 1 iterations     average: 1803.623438ms
Fox             : 12.467032 seconds for 1 iterations    average: 12467.031717ms
Volodarski2     : 0.891086 seconds for 3 iterations     average: 297.028542ms

РЕШЕНИЕ С КЕШИРОВАНИЕМ СТЕПЕНЕЙ

Я тоже добавлю пять копеек.

Уже говорили, что число имеет ровно пять нечётных делителей, если оно представимо в виде 2^k * p^4, где p > 2 - простое число?

Вот на этом факте и построен решатель. Для ускорения счёта кэшируется всё, что можно:

  • для простых чисел кешируются квадрат и четвертая степень
  • для проверки, подходит ли простое число, используются логарифмы - они тоже кешируются.
"""
Найдите все натуральные числа, принадлежащие отрезку [35000000;40000000], у которых ровно пять различных нечётных делителей (количество чётных делителей может быть любым). В ответе перечислите найденные числа в порядке возрастания.

Для решения задачи воспользуемся тем, что число имеет ровно 5 различных нечётных делителей, только если оно представимо в виде 2^k * p^4, где k - степень двойки, p - простое число.
"""
import math
import time
from typing import Any, Generator


class Number:
    """
    Число n вместе с кешированными значениями n^2 и n^4

    - n (number) - само число
    - _n2 (number) - n^2, используется для ускорения проверки на простоту 
    - _n4 (number) - n^4, используется для ускорения вычислений
    - _log2_n4 (number) - логарифм n^4 по основанию 2, используется для ускорения вычислений
    """

    def __init__(self, number: int):
        self.n = number
        self._n2 = number * number
        self._n4 = self._n2 * self._n2

        # В решателе используются логарифмы, поэтому кешируем их
        self._log2_n4 = math.log2(self._n4)

    def __repr__(self):
        return f"Number({self.n})"


class Result:
    """
    Результат вычислений.

    - prime (Number) - простое число.
    - degree (int) - степень двойки
    - value (int) - число вида 2^degree * prime^4
    """

    def __init__(self, prime: Number, degree: int):
        self.prime = prime
        self.degree = degree
        self.value = (1 << degree)*self.prime._n4

    def __repr__(self):
        return f"Result({self.prime}, {self.degree}, value={self.value})"


PRIMES_100 = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37,
              41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]


class Solver:
    """
    Решатель задачи. Находит все числа вида 2^k * p^4, где k - степень двойки, p - простое число в заданном диапазоне.


    - left (int) - левая граница диапазона (включительно)
    - right (int) - правая граница диапазона (включительно)
    """

    def __init__(self, left: int, right: int):
        self.left = min(left, right)
        self.right = max(left, right)
        if self.left < 2:
            raise ValueError("Left bound must be at least 2.")
        # В решателе используются логарифмы, поэтому кешируем их
        self._log2_left = math.log2(self.left)
        self._log2_right = math.log2(self.right)

        # Список простых чисел. Нужно обеспечить, что в нём есть все простые числа до \sqrt[4](right)
        # начинаем с простых чисел до 100
        self.primes = list(map(Number, PRIMES_100))
        # Добавляем простые числа до \sqrt[4](right)
        while self.primes[-1]._n4 < self.right:
            self.add_next_prime()

    def add_next_prime(self) -> Number:
        """
        Находит следующее простое число и добавляет его в список простых чисел.
        """
        p = self.primes[-1].n + 2
        while not self.is_prime(p):
            p += 2
        self.primes.append(Number(p))
        return self.primes[-1]

    def is_prime(self, n: int) -> bool:
        """
        Проверяет, является ли число простым.

        Более точно, проверяет, что среди self.primes нет делителей числа n.
        """
        assert (n >= 2)
        for prime in self.primes:
            if prime._n2 > n:
                break
            if n % prime.n == 0:
                return False
        return True

    def is_proper_num(self, p: Number) -> list[Result]:
        """
        Проверяет, является ли число p подходящим под условия задачи.

        Если число не подходит, возвращает пустой список.
        Если число подходит, возвращает список Result(p, k), где k - степень двойки, при которой 2^k * p^4 попадает в заданный диапазон.
        """
        if p._n4 > self.right:
            return []
        if p._n4 > self.left:
            return [Result(p, 0)]
        # Минимальная степень двойки, при которой 2^k * p^4 >= left
        deg = math.ceil(self._log2_left - p._log2_n4)
        # Максимальная степень двойки, при которой 2^k * p^4 <= right
        lim = math.floor(self._log2_right - p._log2_n4)

        if deg > lim:
            # В заданном диапазоне нет чисел вида 2^k * p^4
            return []
        return [Result(p, d) for d in range(deg, lim + 1)]

    def gen_proper_odd_primes(self) -> Generator[Result, None, None]:
        """
        Генератор, который перечисляет Result(p,k), то есть числа вида 2^k * p^4, где k - степень двойки, p - простое число, и 2^k * P^4 находится в заданном диапазоне.
        """
        for p in self.primes[1:]:
            yield from self.is_proper_num(p)
            # Если p^4 > right, то дальше нет смысла проверять
            if p._n4 > self.right:
                break

    def solve(self) -> list[Result]:
        """
        Решает задачу, находя все числа вида 2^k * p^4, где k - степень двойки, p - простое число в заданном диапазоне.

        Возвращает список объектов Result, соответствующих найденным простым числам.
        """
        return list(self.gen_proper_odd_primes())



if __name__ == "__main__":
    left = 35_000_000
    right = 40_000_000
    start_time = time.time()
    solver = Solver(left, right)
    results = solver.solve()
    end_time = time.time()
    print(f"Time taken: {end_time - start_time:.6f} seconds")

    for result in results:
        print(result)
    numbers = sorted([r.value for r in results])
    for n in numbers:
        print(n)

Вывод:

Time taken: 0.000195 seconds
Result(Number(7), 14, value=39337984)
Result(Number(23), 7, value=35819648)
Result(Number(47), 3, value=39037448)
Result(Number(79), 0, value=38950081)
35819648
38950081
39037448
39337984

Сравнение результатов

Небольшой бенчмарк

# Сравнение результатов

def fox(left, right):
    # Функция: проверка, является ли число простым (достаточно до sqrt)
    def is_prime(v_n):
        if v_n < 2:
            return False
        for v_i in range(2, int(v_n ** 0.5) + 1):
            if v_n % v_i == 0:
                return False
        return True

    # Список для хранения результатов
    lst_results = []

    # Перебираем все нечётные простые числа p, такие что p^4 * 2^k попадает в диапазон
    for v_p in range(3, 1000, 2):  # 1000 — с запасом, т.к. 999^4 > 1e12
        if is_prime(v_p):
            v_p4 = v_p ** 4  # Вычисляем p^4

            v_n = v_p4
            while v_n <= right:
                if v_n >= left:
                    # Получаем и сортируем нечётные делители (всегда 5 штук)
                    lst_divs = [1, v_p, v_p**2, v_p**3, v_p**4]
                    lst_results.append((v_n, lst_divs))
                v_n *= 2  # Умножаем на степень двойки
    return lst_results

def volodarski(left, right):

    def is_prime(n):
        return all(n % i != 0 for i in range(2, n))


    def numbers(m, n):
        i = 3
        while True:
            if is_prime(i):
                j = i ** 4
                if n < j:
                    break
                while j <= n:
                    if m <= j:
                        yield j
                    j *= 2
            i += 2

    return list(numbers(left, right))

# benchmark
def benchmark(f, N = 1000):
    """
    Функция для бенчмарка функции f.
    """
    start_time = time.time()
    for _ in range(N):
        f()
    end_time = time.time()
    print(f"Time taken for {N} iterations: {end_time - start_time:.6f} seconds")
    dt = (end_time - start_time) / N
    print(f"Average time taken per iteration: {dt:.6f} seconds")


if __name__ == "__main__":
    N = 1000
    left = 35_000_000
    right = 40_000_000
    for i in [1, 10, 100, 1000]:
        left *= i
        right *= i
        print(f"# Testing with left={left}, right={right}")
        print("## Solver")
        benchmark(lambda: Solver(left, right).solve(), N)
        print("## Fox")
        benchmark(lambda: fox(left, right), N)
        print("## Volodarski")
        benchmark(lambda: volodarski(left, right), N if N < 1000 else 10)
# Testing with left=35000000, right=40000000
## Solver
Time taken for 1000 iterations: 0.073144 seconds
Average time taken per iteration: 0.000073 seconds
## Fox
Time taken for 1000 iterations: 1.035514 seconds
Average time taken per iteration: 0.001036 seconds
## Volodarski
Time taken for 10 iterations: 0.001307 seconds
Average time taken per iteration: 0.000131 seconds
# Testing with left=350000000, right=400000000
## Solver
Time taken for 1000 iterations: 0.058008 seconds
Average time taken per iteration: 0.000058 seconds
## Fox
Time taken for 1000 iterations: 0.505826 seconds
Average time taken per iteration: 0.000506 seconds
## Volodarski
Time taken for 10 iterations: 0.002486 seconds
Average time taken per iteration: 0.000249 seconds
# Testing with left=35000000000, right=40000000000
## Solver
Time taken for 1000 iterations: 0.248829 seconds
Average time taken per iteration: 0.000249 seconds
## Fox
Time taken for 1000 iterations: 0.624457 seconds
Average time taken per iteration: 0.000624 seconds
## Volodarski
Time taken for 10 iterations: 0.023346 seconds
Average time taken per iteration: 0.002335 seconds
# Testing with left=35000000000000, right=40000000000000
## Solver
Time taken for 1000 iterations: 1.705527 seconds
Average time taken per iteration: 0.001706 seconds
## Fox
Time taken for 1000 iterations: 0.723012 seconds
Average time taken per iteration: 0.000723 seconds
## Volodarski
Time taken for 10 iterations: 0.647147 seconds
Average time taken per iteration: 0.064715 seconds
→ Ссылка
Автор решения: Danis
def solve(left, right):
    primes_max = math.isqrt(math.isqrt(right)+1)+1
    
    primes = bytearray(primes_max)
    primes[0] = primes[1] = 1
    for i in range(math.isqrt(primes_max)+2):
        if primes[i]:
            continue
        
        primes[i*i::i] = [1]*len(range(i*i,primes_max,i))
    
    
    left_log2 = math.log2(left)
    right_log2 = math.log2(right)
    
    primes = enumerate(primes)
    next(primes); next(primes); next(primes)
    
    res = []
    for p, f in primes:
        if f: 
            continue

        value = p**4
        value_log2 = math.log2(value)
        
        deg = math.ceil(left_log2 - value_log2)
        lim = math.floor(right_log2 - value_log2)
        
        value <<= deg
        for i in range(deg, lim + 1):
            res.append(value)
            value <<= 1
    return sorted(res)

Данный код не особо сильно отличается от Volodarski2, но вроде показывает чуть более быстрый результат.

→ Ссылка