Как сделать Сито Эратосфена более эффективным в памяти в python? - PullRequest
4 голосов
/ 14 июля 2020

Проблема ограничения памяти сита Эратосфена

В настоящее время я пытаюсь реализовать версию сита Эратосфена для проблемы Каттиса, однако я сталкиваюсь с некоторыми ограничениями памяти, которые моя реализация не пройдет.

Вот ссылка на постановку задачи . Короче говоря, проблема требует, чтобы я сначала вернул количество простых чисел, меньшее или равное n , а затем решаю для определенного количества запросов, является ли число i простым или нет. Существует ограничение на использование памяти 50 МБ, а также использование только стандартных библиотек python (нет numpy et c). Ограничение памяти - вот где я застрял.

Вот мой код:

import sys

def sieve_of_eratosthenes(xs, n):
    count = len(xs) + 1
    p = 3 # start at three
    index = 0
    while p*p < n:
        for i in range(index + p, len(xs), p):
            if xs[i]:
                xs[i] = 0
                count -= 1

        temp_index = index
        for i in range(index + 1, len(xs)):
            if xs[i]:
                p = xs[i]
                temp_index += 1
                break
            temp_index += 1
        index = temp_index

    return count


def isPrime(xs, a):
    if a == 1:
        return False
    if a == 2:
        return True
    if not (a & 1):
        return False
    return bool(xs[(a >> 1) - 1])

def main():
    n, q = map(int, sys.stdin.readline().split(' '))
    odds = [num for num in range(2, n+1) if (num & 1)]
    print(sieve_of_eratosthenes(odds, n))

    for _ in range(q):
        query = int(input())
        if isPrime(odds, query):
            print('1')
        else:
            print('0')


if __name__ == "__main__":
    main()

Я сделал некоторые улучшения, например, сохранил только список всех нечетных чисел, которые вдвое уменьшает использование памяти. Я также уверен, что при вычислении простых чисел код работает так, как задумано (без неправильного ответа). У меня вопрос: как сделать мой код еще более эффективным с точки зрения памяти? Стоит ли использовать другие структуры данных? Заменить мой список целых чисел логическими? Bitarray?

Любые советы очень ценны!

EDIT

После некоторой настройки кода в python я наткнулся на стену, где моя реализация сегментированного сита не могла передать требования к памяти.

Вместо этого я решил реализовать решение в Java, что потребовало очень небольших усилий. Вот код:

  public int sieveOfEratosthenes(int n){
    sieve = new BitSet((n+1) / 2);
    int count = (n + 1) / 2;

    for (int i=3; i*i <= n; i += 2){
      if (isComposite(i)) {
        continue;
      }

      // Increment by two, skipping all even numbers
      for (int c = i * i; c <= n; c += 2 * i){
        if(!isComposite(c)){
          setComposite(c);
          count--;
        }
      }
    }

    return count;

  }

  public boolean isComposite(int k) {
    return sieve.get((k - 3) / 2); // Since we don't keep track of even numbers
  }

  public void setComposite(int k) {
    sieve.set((k - 3) / 2); // Since we don't keep track of even numbers
  }

  public boolean isPrime(int a) {
    if (a < 3)
      return a > 1;

    if (a == 2)
      return true;

    if ((a & 1) == 1)
      return !isComposite(a);
    else
      return false;

  }

  public void run() throws Exception{
    BufferedReader scan = new BufferedReader(new InputStreamReader(System.in));
    String[] line = scan.readLine().split(" ");

    int n = Integer.parseInt(line[0]); int q = Integer.parseInt(line[1]);
    System.out.println(sieveOfEratosthenes(n));

    for (int i=0; i < q; i++){
      line = scan.readLine().split(" ");
      System.out.println( isPrime(Integer.parseInt(line[0])) ? '1' : '0');
    }
  }

Я лично не нашел способа реализовать это решение BitSet в Python (используя только стандартную библиотеку).

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

Ответы [ 3 ]

2 голосов
/ 15 июля 2020

Есть трюк, который я изучил только вчера - если вы разделите числа на группы по 6, только 2 из 6 могут быть простыми. Остальные могут быть равномерно разделены на 2 или 3. Это означает, что для отслеживания простоты 6 чисел требуется всего 2 бита; байт, содержащий 8 бит, может отслеживать простоту 24 чисел! Это значительно снижает требования к памяти вашего сита.

В Python 3.7.5 64 бит на Windows 10 следующий код go не превышал 36,4 МБ.

remainder_bit = [0, 0x01, 0, 0, 0, 0x02,
                 0, 0x04, 0, 0, 0, 0x08,
                 0, 0x10, 0, 0, 0, 0x20,
                 0, 0x40, 0, 0, 0, 0x80]

def is_prime(xs, a):
    if a <= 3:
        return a > 1
    index, rem = divmod(a, 24)
    bit = remainder_bit[rem]
    if not bit:
        return False
    return not (xs[index] & bit)

def sieve_of_eratosthenes(xs, n):
    count = (n // 3) + 1 # subtract out 1 and 4, add 2 3 and 5
    p = 5
    while p*p <= n:
        if is_prime(xs, p):
            for i in range(5 * p, n + 1, p):
                index, rem = divmod(i, 24)
                bit = remainder_bit[rem]
                if bit and not (xs[index] & bit):
                    xs[index] |= bit
                    count -= 1
        p += 2
        if is_prime(xs, p):
            for i in range(5 * p, n + 1, p):
                index, rem = divmod(i, 24)
                bit = remainder_bit[rem]
                if bit and not (xs[index] & bit):
                    xs[index] |= bit
                    count -= 1
        p += 4

    return count


def init_sieve(n):
    return bytearray((n + 23) // 24)

n = 100000000
xs = init_sieve(n)
sieve_of_eratosthenes(xs, n)
5761455
sum(is_prime(xs, i) for i in range(n+1))
5761455
1 голос
/ 15 июля 2020

Это действительно очень сложная задача. При максимально возможном N 10 ^ 8 использование одного байта на значение приводит к почти 100 МБ данных без каких-либо накладных расходов. Даже если вдвое уменьшить объем данных за счет сохранения только нечетных чисел, с учетом накладных расходов вы приблизитесь к 50 МБ.

Это означает, что решение должно будет использовать одну или несколько из нескольких стратегий:

  1. Использование более эффективного типа данных для нашего массива флагов простоты. Python списки содержат массив указателей на каждый элемент списка (4 байта каждый на 64-битном python). Нам действительно нужно необработанное двоичное хранилище, которое практически оставляет только bytearray в стандартном python.
  2. Использование только одного бита на значение в сите вместо целого байта (технически Bool нужен только один бит, но обычно используется полный байт).
  3. Подразделение для удаления четных чисел и, возможно, также кратных 3, 5, 7 и т. д. c.
  4. Использование сегментированное решето

Сначала я пытался решить проблему, сохраняя только 1 бит на значение в сите, и хотя использование памяти действительно соответствовало требованиям, Python ' s медленные манипуляции с битами слишком увеличивают время выполнения. Также было довольно сложно определить сложную индексацию, чтобы убедиться, что правильные биты подсчитываются надежно.

Затем я реализовал решение только для нечетных чисел, используя массив байтов, и хотя это было немного быстрее, память по-прежнему была проблемой.

Реализация нечетных чисел в массиве байтов:

class Sieve:
    def __init__(self, n):
        self.not_prime = bytearray(n+1)
        self.not_prime[0] = self.not_prime[1] = 1
        for i in range(2, int(n**.5)+1):
            if self.not_prime[i] == 0:
                self.not_prime[i*i::i] = [1]*len(self.not_prime[i*i::i])
        self.n_prime = n + 1 - sum(self.not_prime)
        
    def is_prime(self, n):
        return int(not self.not_prime[n])
        


def main():
    n, q = map(int, input().split())
    s = Sieve(n)
    print(s.n_prime)
    for _ in range(q):
        i = int(input())
        print(s.is_prime(i))

if __name__ == "__main__":
    main()

Дальнейшее уменьшение объема памяти из-за этого * должно * заставить его работать. * также удаление кратных 2 и 3 не показалось достаточным сокращением памяти, хотя guppy.hpy().heap(), казалось, предполагало, что мое использование было немного меньше 50 МБ. ?‍♂️

0 голосов
/ 14 июля 2020

Я думаю, вы можете попробовать использовать список логических значений, чтобы отметить, является ли его индекс простым или нет:

def sieve_of_erato(range_max):
    primes_count = range_max
    is_prime = [True for i in range(range_max + 1)]
    # Cross out all even numbers first.
    for i in range(4, range_max, 2):
        is_prime[i] = False
        primes_count -=1
    i = 3
    while i * i <= range_max:
        if is_prime[i]:
            # Update all multiples of this prime number
            # CAREFUL: Take note of the range args.
            # Reason for i += 2*i instead of i += i:
            # Since p and p*p, both are odd, (p*p + p) will be even,
            # which means that it would have already been marked before
            for multiple in range(i * i, range_max + 1, i * 2):
                is_prime[multiple] = False
                primes_count -= 1
        i += 1

    return primes_count


def main():
    num_primes = sieve_of_erato(100)
    print(num_primes)


if __name__ == "__main__":
    main()

Вы можете использовать массив is_prime, чтобы проверить, является ли число простым или нет позже, просто отметив is_prime[number] == True.

Если это не сработает, попробуйте сегментированное решето .

В качестве бонуса вы можете быть удивлены, узнав, что есть способ создать сито в O(n), а не в O(nloglogn). Проверьте код здесь .

...